From 954090b13819e493424c88a9a54225f142a162fb Mon Sep 17 00:00:00 2001 From: stevenbooke Date: Tue, 27 Feb 2024 18:05:06 -0800 Subject: [PATCH] Trezor support for Solana CLI --- Cargo.lock | 128 +++++++++++++ Cargo.toml | 2 + docs/src/cli/wallets/hardware/index.md | 1 + docs/src/cli/wallets/hardware/trezor.md | 175 ++++++++++++++++++ docs/src/cli/wallets/index.md | 2 +- remote-wallet/Cargo.toml | 3 + remote-wallet/src/lib.rs | 1 + remote-wallet/src/locator.rs | 207 +++++++++++++++++++-- remote-wallet/src/remote_keypair.rs | 38 ++-- remote-wallet/src/remote_wallet.rs | 137 +++++++++++++- remote-wallet/src/trezor.rs | 227 ++++++++++++++++++++++++ 11 files changed, 892 insertions(+), 29 deletions(-) create mode 100644 docs/src/cli/wallets/hardware/trezor.md create mode 100644 remote-wallet/src/trezor.rs diff --git a/Cargo.lock b/Cargo.lock index 528c7d00f33d04..cdc9b220fc606d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -574,6 +574,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "bech32" +version = "0.10.0-beta" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" + [[package]] name = "bincode" version = "1.3.3" @@ -619,6 +625,36 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bitcoin" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd00f3c09b5f21fb357abe32d29946eb8bb7a0862bae62c0b5e4a692acbbe73c" +dependencies = [ + "bech32", + "bitcoin-internals", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1", +] + +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -2399,6 +2435,18 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed443af458ccb6d81c1e7e661545f94d3176752fb1df2f543b902a1e0f51e2" + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + [[package]] name = "hidapi" version = "2.6.0" @@ -2996,6 +3044,18 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "libusb1-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d0e2afce4245f2c9a418511e5af8718bcaf2fa408aefb259504d1a9cb25f27" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.1.3" @@ -4059,6 +4119,17 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "protobuf" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror", +] + [[package]] name = "protobuf-src" version = "1.1.0+21.5" @@ -4068,6 +4139,15 @@ dependencies = [ "autotools", ] +[[package]] +name = "protobuf-support" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c" +dependencies = [ + "thiserror", +] + [[package]] name = "qstring" version = "0.7.2" @@ -4499,6 +4579,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rusb" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45fff149b6033f25e825cbb7b2c625a11ee8e6dac09264d49beb125e39aa97bf" +dependencies = [ + "libc", + "libusb1-sys", +] + [[package]] name = "rustc-demangle" version = "0.1.21" @@ -4682,6 +4772,25 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "secp256k1" +version = "0.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" +dependencies = [ + "bitcoin_hashes", + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" +dependencies = [ + "cc", +] + [[package]] name = "security-framework" version = "2.4.2" @@ -6721,6 +6830,7 @@ name = "solana-remote-wallet" version = "1.19.0" dependencies = [ "assert_matches", + "bitcoin", "console", "dialoguer", "hidapi", @@ -6730,8 +6840,10 @@ dependencies = [ "parking_lot 0.12.1", "qstring", "semver 1.0.22", + "serial_test", "solana-sdk", "thiserror", + "trezor-client", "uriparse", ] @@ -8692,6 +8804,22 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de5f738ceab88e2491a94ddc33c3feeadfa95fedc60363ef110845df12f3878" +[[package]] +name = "trezor-client" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f62c95b37f6c769bd65a0d0beb8b2b003e72998003b896a616a6777c645c05ed" +dependencies = [ + "bitcoin", + "byteorder", + "hex", + "protobuf", + "rusb", + "thiserror", + "tracing", + "unicode-normalization", +] + [[package]] name = "try-lock" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 66436c9cfb3fd8..cf2f172d593912 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -156,6 +156,7 @@ atty = "0.2.11" backoff = "0.4.0" base64 = "0.21.7" bincode = "1.3.3" +bitcoin = "0.31.1" bitflags = { version = "2.4.2", features = ["serde"] } blake3 = "1.5.0" block-buffer = "0.10.4" @@ -423,6 +424,7 @@ toml = "0.8.10" tonic = "0.9.2" tonic-build = "0.9.2" trees = "0.4.2" +trezor-client = { version = "0.1.3", features = ["solana"]} tungstenite = "0.20.1" uriparse = "0.6.4" url = "2.5.0" diff --git a/docs/src/cli/wallets/hardware/index.md b/docs/src/cli/wallets/hardware/index.md index 30f53f86d3a3d8..1d4602d17a1758 100644 --- a/docs/src/cli/wallets/hardware/index.md +++ b/docs/src/cli/wallets/hardware/index.md @@ -23,6 +23,7 @@ hardware wallet. The Solana CLI supports the following hardware wallets: - [Ledger Nano S and Ledger Nano X](./ledger.md) +- [Trezor Model T and Trezor Safe 3](./trezor.md) ## Specify a Keypair URL diff --git a/docs/src/cli/wallets/hardware/trezor.md b/docs/src/cli/wallets/hardware/trezor.md new file mode 100644 index 00000000000000..6adbd0dd8b613c --- /dev/null +++ b/docs/src/cli/wallets/hardware/trezor.md @@ -0,0 +1,175 @@ +--- +title: Using Trezor Hardware Wallets in the Solana CLI +pagination_label: "Hardware Wallets in the Solana CLI: Trezor" +sidebar_label: Trezor +--- + +This page describes how to use a Trezor Model T or Trezor Safe 3 device to +interact with Solana using the command line tools. + +## Before You Begin + +- [Install the Solana command-line tools](../../install.md) +- [Review Trezor and BIP-32](https://trezor.io/learn/a/what-is-bip32) +- [Review Trezor and BIP-44](https://trezor.io/learn/a/what-is-bip44) + +## Use Trezor Model T or Trezor Safe 3 with Solana CLI + +1. Plug your Trezor Model T or Trezor Safe 3 device into your computer's USB port + +### View your Wallet Addresses + +On your computer, run: + +```bash +solana-keygen pubkey usb://trezor?key=0/0 +``` + +This confirms your Trezor device is connected properly and in the correct state +to interact with the Solana CLI. The command returns your Trezor device's first Solana account's +external (receiving) wallet address using the [BIP-32](https://trezor.io/learn/a/what-is-bip32) derivation path `m/44'/501'/0'/0'`. + +Your Trezor device supports an arbitrary number of valid wallet addresses and signers. To +view any address, use the `solana-keygen pubkey` command, as shown below, +followed by a valid [keypair URL](./index.md#specify-a-keypair-url). + +Multiple wallet addresses can be useful if you want to transfer tokens between +your own accounts for different purposes, or use different keypairs on the +device as signing authorities for a stake account, for example. + +All of the following commands will display different addresses, associated with +the keypair path given. Try them out! + +```bash +solana-keygen pubkey usb://trezor?key=0/0 +solana-keygen pubkey usb://trezor?key=0/1 +solana-keygen pubkey usb://trezor?key=1/0 +solana-keygen pubkey usb://trezor?key=1/1 +``` + +- NOTE: keypair url parameters are ignored in **zsh** +  [see troubleshooting for more info](#troubleshooting) + +You can use other values for the number after `key=` as well. Any of the +addresses displayed by these commands are valid Solana wallet addresses. The +private portion associated with each address is stored securely on the Trezor device, and +is used to sign transactions from this address. Just make a note of which +keypair URL you used to derive any address you will be using to receive tokens. + +If you are only planning to use a single address/keypair on your device, a good +easy-to-remember path might be to use the address at `key=0/`. View this address +with: + +```bash +solana-keygen pubkey usb://trezor?key=0/0 +solana-keygen pubkey usb://trezor?key=0/1 +``` + +Now you have a wallet address (or multiple addresses), you can share any of +these addresses publicly to act as a receiving address, and you can use the +associated keypair URL as the signer for transactions from that address. + +### View your Balance + +To view the balance of any account, regardless of which wallet it uses, use the +`solana balance` command: + +```bash +solana balance SOME_WALLET_ADDRESS +``` + +For example, if your address is `7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri`, +then enter the following command to view the balance: + +```bash +solana balance 7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri +``` + +You can also view the balance of any account address on the Accounts tab in the +[Explorer](https://explorer.solana.com/accounts) and paste the address in the +box to view the balance in your web browser. + +Note: Any address with a balance of 0 SOL, such as a newly created one on your +Trezor, will show as "Not Found" in the explorer. Empty accounts and +non-existent accounts are treated the same in Solana. This will change when your +account address has some SOL in it. + +### Send SOL from a Trezor + +To send some tokens from an address controlled by your Trezor, you will need to +use the device to sign a transaction, using the same keypair URL you used to +derive the address. To do this, make sure your Trezor is plugged in. + +The `solana transfer` command is used to specify to which address to send +tokens, how many tokens to send, and uses the `--keypair` argument to specify +which keypair is sending the tokens, which will sign the transaction, and the +balance from the associated address will decrease. + +```bash +solana transfer RECIPIENT_ADDRESS AMOUNT --keypair KEYPAIR_URL_OF_SENDER +``` + +Below is a full example. First, an address is viewed at a certain keypair URL. +Second, the balance of that address is checked. Lastly, a transfer transaction +is entered to send `1` SOL to the recipient address +`7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri`. When you hit Enter for a +transfer command, you will be prompted to approve the transaction details on +your Trezor device. Follow the prompts on the device and review the +transaction details. If they look correct, follow the prompts on your device. + +```bash +~$ solana-keygen pubkey usb://trezor?key=0/0 +CjeqzArkZt6xwdnZ9NZSf8D1CNJN1rjeFiyd8q7iLWAV + +~$ solana balance CjeqzArkZt6xwdnZ9NZSf8D1CNJN1rjeFiyd8q7iLWAV +1.000005 SOL + +~$ solana transfer 7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri 1 --keypair usb://trezor?key=0/0 +Waiting for your approval on Trezor hardware wallet +✅ Approved + +Signature: kemu9jDEuPirKNRKiHan7ycybYsZp7pFefAdvWZRq5VRHCLgXTXaFVw3pfh87MQcWX4kQY4TjSBmESrwMApom1V +``` + +After approving the transaction on your device, the program will display the +transaction signature, and wait for the maximum number of confirmations (32) +before returning. This only takes a few seconds, and then the transaction is +finalized on the Solana network. You can view details of this or any other +transaction by going to the Transaction tab in the +[Explorer](https://explorer.solana.com/transactions) and paste in the +transaction signature. + +## Troubleshooting + +### Keypair URL parameters are ignored in zsh + +The question mark character is a special character in zsh. If that's not a +feature you use, add the following line to your `~/.zshrc` to treat it as a +normal character: + +```bash +unsetopt nomatch +``` + +Then either restart your shell window or run `~/.zshrc`: + +```bash +source ~/.zshrc +``` + +If you would prefer not to disable zsh's special handling of the question mark +character, you can disable it explicitly with a backslash in your keypair URLs. +For example: + +```bash +solana-keygen pubkey usb://trezor\?key=0/0 +``` + +## Support + +You can find additional support and get help on the +[Solana StackExchange](https://solana.stackexchange.com). + +Read more about [sending and receiving tokens](../../examples/transfer-tokens.md) and +[delegating stake](../../examples/delegate-stake.md). You can use your Ledger keypair +URL anywhere you see an option or argument that accepts a ``. diff --git a/docs/src/cli/wallets/index.md b/docs/src/cli/wallets/index.md index 9643ef61ec13eb..ee485e3fc88d10 100644 --- a/docs/src/cli/wallets/index.md +++ b/docs/src/cli/wallets/index.md @@ -54,7 +54,7 @@ some interface for signing transactions. ### Hardware Wallet Security A hardware wallet, such as the -[Ledger hardware wallet](https://www.ledger.com/), offers a great blend of +[Ledger hardware wallet](https://www.ledger.com/) and [Trezor hardware wallet](https://trezor.io/), offers a great blend of security and convenience for cryptocurrencies. It effectively automates the process of offline signing while retaining nearly all the convenience of a file system wallet. diff --git a/remote-wallet/Cargo.toml b/remote-wallet/Cargo.toml index 8cea360d7c14ca..607e6c75f3e8c0 100644 --- a/remote-wallet/Cargo.toml +++ b/remote-wallet/Cargo.toml @@ -10,6 +10,7 @@ license = { workspace = true } edition = { workspace = true } [dependencies] +bitcoin = { workspace = true } console = { workspace = true } dialoguer = { workspace = true } hidapi = { workspace = true, optional = true } @@ -21,10 +22,12 @@ qstring = { workspace = true } semver = { workspace = true } solana-sdk = { workspace = true } thiserror = { workspace = true } +trezor-client = { workspace = true } uriparse = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } +serial_test = { workspace = true } [features] default = ["linux-static-hidraw", "hidapi"] diff --git a/remote-wallet/src/lib.rs b/remote-wallet/src/lib.rs index 2400850734b1c1..ec60cff5ea3e66 100644 --- a/remote-wallet/src/lib.rs +++ b/remote-wallet/src/lib.rs @@ -5,3 +5,4 @@ pub mod ledger_error; pub mod locator; pub mod remote_keypair; pub mod remote_wallet; +pub mod trezor; diff --git a/remote-wallet/src/locator.rs b/remote-wallet/src/locator.rs index 613824990211f6..0e1c3a1bb715a2 100644 --- a/remote-wallet/src/locator.rs +++ b/remote-wallet/src/locator.rs @@ -12,6 +12,7 @@ use { pub enum Manufacturer { Unknown, Ledger, + Trezor, } impl Default for Manufacturer { @@ -22,6 +23,7 @@ impl Default for Manufacturer { const MANUFACTURER_UNKNOWN: &str = "unknown"; const MANUFACTURER_LEDGER: &str = "ledger"; +const MANUFACTURER_TREZOR: &str = "trezor"; #[derive(Clone, Debug, Error, PartialEq, Eq)] #[error("not a manufacturer")] @@ -39,6 +41,7 @@ impl FromStr for Manufacturer { let s = s.to_ascii_lowercase(); match s.as_str() { MANUFACTURER_LEDGER => Ok(Self::Ledger), + MANUFACTURER_TREZOR => Ok(Self::Trezor), _ => Err(ManufacturerError), } } @@ -56,6 +59,7 @@ impl AsRef for Manufacturer { match self { Self::Unknown => MANUFACTURER_UNKNOWN, Self::Ledger => MANUFACTURER_LEDGER, + Self::Trezor => MANUFACTURER_TREZOR, } } } @@ -173,7 +177,11 @@ mod tests { matches!(Manufacturer::from_str(MANUFACTURER_LEDGER), Ok(v) if v == Manufacturer::Ledger) ); assert_eq!(Manufacturer::Ledger.as_ref(), MANUFACTURER_LEDGER); - + assert_eq!(MANUFACTURER_TREZOR.try_into(), Ok(Manufacturer::Trezor)); + assert!( + matches!(Manufacturer::from_str(MANUFACTURER_TREZOR), Ok(v) if v == Manufacturer::Trezor) + ); + assert_eq!(Manufacturer::Trezor.as_ref(), MANUFACTURER_TREZOR); assert!( matches!(Manufacturer::from_str("bad-manufacturer"), Err(e) if e == ManufacturerError) ); @@ -212,6 +220,35 @@ mod tests { Ok(e) if e == expect ); + let manufacturer = Manufacturer::Trezor; + let manufacturer_str = "trezor"; + + let expect = Locator { + manufacturer, + pubkey: None, + }; + assert_matches!( + Locator::new_from_parts(manufacturer, None::), + Ok(e) if e == expect + ); + assert_matches!( + Locator::new_from_parts(manufacturer_str, None::), + Ok(e) if e == expect + ); + + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + }; + assert_matches!( + Locator::new_from_parts(manufacturer, Some(pubkey)), + Ok(e) if e == expect + ); + assert_matches!( + Locator::new_from_parts(manufacturer_str, Some(pubkey_str.as_str())), + Ok(e) if e == expect + ); + assert_matches!( Locator::new_from_parts("bad-manufacturer", None::), Err(LocatorError::ManufacturerError(e)) if e == ManufacturerError @@ -309,27 +346,110 @@ mod tests { Err(LocatorError::UnimplementedScheme) ); - // usb://bad-manufacturer + // usb://ledger/bad-pubkey let mut builder = URIReferenceBuilder::new(); builder .try_scheme(Some("usb")) .unwrap() - .try_authority(Some("bad-manufacturer")) + .try_authority(Some(Manufacturer::Ledger.as_ref())) + .unwrap() + .try_path("bad-pubkey") + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + Locator::new_from_uri(&uri), + Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid)) + ); + + let manufacturer = Manufacturer::Trezor; + + // usb://trezor/{PUBKEY}?key=0/0 + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Trezor.as_ref())) + .unwrap() + .try_path(pubkey_str.as_str()) + .unwrap() + .try_query(Some("key=0/0")) + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // usb://trezor/{PUBKEY} + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Trezor.as_ref())) + .unwrap() + .try_path(pubkey_str.as_str()) + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // usb://trezor + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Trezor.as_ref())) + .unwrap() + .try_path("") + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: None, + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // usb://trezor/ + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some(Manufacturer::Trezor.as_ref())) + .unwrap() + .try_path("/") + .unwrap(); + let uri = builder.build().unwrap(); + let expect = Locator { + manufacturer, + pubkey: None, + }; + assert_eq!(Locator::new_from_uri(&uri), Ok(expect)); + + // bad-scheme://trezor + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("bad-scheme")) + .unwrap() + .try_authority(Some(Manufacturer::Trezor.as_ref())) .unwrap() .try_path("") .unwrap(); let uri = builder.build().unwrap(); assert_eq!( Locator::new_from_uri(&uri), - Err(LocatorError::ManufacturerError(ManufacturerError)) + Err(LocatorError::UnimplementedScheme) ); - // usb://ledger/bad-pubkey + // usb://trezor/bad-pubkey let mut builder = URIReferenceBuilder::new(); builder .try_scheme(Some("usb")) .unwrap() - .try_authority(Some(Manufacturer::Ledger.as_ref())) + .try_authority(Some(Manufacturer::Trezor.as_ref())) .unwrap() .try_path("bad-pubkey") .unwrap(); @@ -338,6 +458,21 @@ mod tests { Locator::new_from_uri(&uri), Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid)) ); + + // usb://bad-manufacturer + let mut builder = URIReferenceBuilder::new(); + builder + .try_scheme(Some("usb")) + .unwrap() + .try_authority(Some("bad-manufacturer")) + .unwrap() + .try_path("") + .unwrap(); + let uri = builder.build().unwrap(); + assert_eq!( + Locator::new_from_uri(&uri), + Err(LocatorError::ManufacturerError(ManufacturerError)) + ); } #[test] @@ -386,18 +521,68 @@ mod tests { Err(LocatorError::UnimplementedScheme) ); - // usb://bad-manufacturer - let path = "usb://bad-manufacturer"; + // usb://ledger/bad-pubkey + let path = "usb://ledger/bad-pubkey"; assert_eq!( Locator::new_from_path(path), - Err(LocatorError::ManufacturerError(ManufacturerError)) + Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid)) ); - // usb://ledger/bad-pubkey - let path = "usb://ledger/bad-pubkey"; + let manufacturer = Manufacturer::Trezor; + let path = format!("usb://trezor/{pubkey}?key=0/0"); + Locator::new_from_path(path).unwrap(); + + // usb://trezor/{PUBKEY}?key=0'/0' + let path = format!("usb://trezor/{pubkey}?key=0'/0'"); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // usb://trezor/{PUBKEY} + let path = format!("usb://trezor/{pubkey}"); + let expect = Locator { + manufacturer, + pubkey: Some(pubkey), + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // usb://trezor + let path = "usb://trezor"; + let expect = Locator { + manufacturer, + pubkey: None, + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // usb://trezor/ + let path = "usb://trezor/"; + let expect = Locator { + manufacturer, + pubkey: None, + }; + assert_eq!(Locator::new_from_path(path), Ok(expect)); + + // bad-scheme://trezor + let path = "bad-scheme://trezor"; + assert_eq!( + Locator::new_from_path(path), + Err(LocatorError::UnimplementedScheme) + ); + + // usb://trezor/bad-pubkey + let path = "usb://trezor/bad-pubkey"; assert_eq!( Locator::new_from_path(path), Err(LocatorError::PubkeyError(ParsePubkeyError::Invalid)) ); + + // usb://bad-manufacturer + let path = "usb://bad-manufacturer"; + assert_eq!( + Locator::new_from_path(path), + Err(LocatorError::ManufacturerError(ManufacturerError)) + ); } } diff --git a/remote-wallet/src/remote_keypair.rs b/remote-wallet/src/remote_keypair.rs index d37eefe2427175..05fc318cee5657 100644 --- a/remote-wallet/src/remote_keypair.rs +++ b/remote-wallet/src/remote_keypair.rs @@ -6,6 +6,7 @@ use { RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletManager, RemoteWalletType, }, + trezor::get_trezor_from_info, }, solana_sdk::{ derivation_path::DerivationPath, @@ -30,6 +31,7 @@ impl RemoteKeypair { ) -> Result { let pubkey = match &wallet_type { RemoteWalletType::Ledger(wallet) => wallet.get_pubkey(&derivation_path, confirm_key)?, + RemoteWalletType::Trezor(wallet) => wallet.get_pubkey(&derivation_path, confirm_key)? }; Ok(Self { @@ -51,6 +53,9 @@ impl Signer for RemoteKeypair { RemoteWalletType::Ledger(wallet) => wallet .sign_message(&self.derivation_path, message) .map_err(|e| e.into()), + RemoteWalletType::Trezor(wallet) => wallet + .sign_message(&self.derivation_path, message) + .map_err(|e| e.into()), } } @@ -67,16 +72,27 @@ pub fn generate_remote_keypair( keypair_name: &str, ) -> Result { let remote_wallet_info = RemoteWalletInfo::parse_locator(locator); - if remote_wallet_info.manufacturer == Manufacturer::Ledger { - let ledger = get_ledger_from_info(remote_wallet_info, keypair_name, wallet_manager)?; - let path = format!("{}{}", ledger.pretty_path, derivation_path.get_query()); - Ok(RemoteKeypair::new( - RemoteWalletType::Ledger(ledger), - derivation_path, - confirm_key, - path, - )?) - } else { - Err(RemoteWalletError::DeviceTypeMismatch) + match remote_wallet_info.manufacturer { + Manufacturer::Ledger => { + let ledger = get_ledger_from_info(remote_wallet_info, keypair_name, wallet_manager)?; + let path = format!("{}{}", ledger.pretty_path, derivation_path.get_query()); + Ok(RemoteKeypair::new( + RemoteWalletType::Ledger(ledger), + derivation_path, + confirm_key, + path, + )?) + } + Manufacturer::Trezor => { + let trezor = get_trezor_from_info(remote_wallet_info, keypair_name, wallet_manager)?; + let path = format!("{}{}", trezor.pretty_path, derivation_path.get_query()); + Ok(RemoteKeypair::new( + RemoteWalletType::Trezor(trezor), + derivation_path, + confirm_key, + path, + )?) + } + _ => Err(RemoteWalletError::DeviceTypeMismatch) } } diff --git a/remote-wallet/src/remote_wallet.rs b/remote-wallet/src/remote_wallet.rs index 33d3b7b993909f..e2167f7b409995 100644 --- a/remote-wallet/src/remote_wallet.rs +++ b/remote-wallet/src/remote_wallet.rs @@ -5,6 +5,7 @@ use { ledger::LedgerWallet, ledger_error::LedgerError, locator::{Locator, LocatorError, Manufacturer}, + trezor::TrezorWallet, }, log::*, parking_lot::RwLock, @@ -14,17 +15,20 @@ use { signature::{Signature, SignerError}, }, std::{ - rc::Rc, - time::{Duration, Instant}, + cell::RefCell, rc::Rc, time::{Duration, Instant} }, thiserror::Error, + trezor_client::{ + self, + Trezor, + }, }; const HID_GLOBAL_USAGE_PAGE: u16 = 0xFF00; const HID_USB_DEVICE_CLASS: u8 = 0; /// Remote wallet error. -#[derive(Error, Debug, Clone)] +#[derive(Error, Debug)] pub enum RemoteWalletError { #[error("hidapi error")] Hid(String), @@ -56,6 +60,9 @@ pub enum RemoteWalletError { #[error("pubkey not found for given address")] PubkeyNotFound, + #[error(transparent)] + TrezorError(#[from] trezor_client::error::Error), + #[error("remote wallet operation rejected by the user")] UserCancel, @@ -63,6 +70,27 @@ pub enum RemoteWalletError { LocatorError(#[from] LocatorError), } +impl Clone for RemoteWalletError { + fn clone(&self) -> Self { + match self { + RemoteWalletError::Hid(_) => self.clone(), + RemoteWalletError::DeviceTypeMismatch => self.clone(), + RemoteWalletError::InvalidDevice => self.clone(), + RemoteWalletError::DerivationPathError(_) => self.clone(), + RemoteWalletError::InvalidInput(_) => self.clone(), + RemoteWalletError::InvalidPath(_) => self.clone(), + RemoteWalletError::LedgerError(_) => self.clone(), + RemoteWalletError::NoDeviceFound => self.clone(), + RemoteWalletError::Protocol(_) => self.clone(), + RemoteWalletError::PubkeyNotFound => self.clone(), + RemoteWalletError::TrezorError(_) => self.clone(), + RemoteWalletError::UserCancel => self.clone(), + RemoteWalletError::LocatorError(_) => self.clone(), + } + } +} + + #[cfg(feature = "hidapi")] impl From for RemoteWalletError { fn from(err: hidapi::HidError) -> RemoteWalletError { @@ -78,6 +106,7 @@ impl From for SignerError { RemoteWalletError::InvalidDevice => SignerError::Connection(err.to_string()), RemoteWalletError::InvalidInput(input) => SignerError::InvalidInput(input), RemoteWalletError::LedgerError(e) => SignerError::Protocol(e.to_string()), + RemoteWalletError::TrezorError(e) => SignerError::Protocol(e.to_string()), RemoteWalletError::NoDeviceFound => SignerError::NoDeviceFound, RemoteWalletError::Protocol(e) => SignerError::Protocol(e.to_string()), RemoteWalletError::UserCancel => { @@ -93,15 +122,17 @@ pub struct RemoteWalletManager { #[cfg(feature = "hidapi")] usb: Arc>, devices: RwLock>, + trezor_client: Option>>, } impl RemoteWalletManager { /// Create a new instance. #[cfg(feature = "hidapi")] - pub fn new(usb: Arc>) -> Rc { + pub fn new(usb: Arc>, trezor_client: Option>>) -> Rc { Rc::new(Self { usb, devices: RwLock::new(Vec::new()), + trezor_client, }) } @@ -205,6 +236,19 @@ impl RemoteWalletManager { } false } + + pub fn has_trezor_client(&self) -> bool { + self.trezor_client.is_some() + } + + pub fn get_trezor_client_clone(&self) -> Option>> { + self.trezor_client.as_ref().map(|trezor| Rc::clone(trezor)) + } + + pub fn get_trezor_wallet(&self, pretty_path: String) -> Result, RemoteWalletError> { + Ok(Rc::new(TrezorWallet::new(self.get_trezor_client_clone(), pretty_path))) + } + } /// `RemoteWallet` trait @@ -262,6 +306,7 @@ pub struct Device { #[derive(Debug)] pub enum RemoteWalletType { Ledger(Rc), + Trezor(Rc), } /// Remote wallet information. @@ -311,7 +356,18 @@ pub fn is_valid_hid_device(usage_page: u16, interface_number: i32) -> bool { #[cfg(feature = "hidapi")] pub fn initialize_wallet_manager() -> Result, RemoteWalletError> { let hidapi = Arc::new(Mutex::new(hidapi::HidApi::new()?)); - Ok(RemoteWalletManager::new(hidapi)) + let trezor_client = match trezor_client::unique(false) { + Ok(mut trezor) => { + match trezor.init_device(None) { + Ok(_) => { + Some(Rc::new(RefCell::new(trezor))) + }, + Err(_) => None, + } + } + Err(_) => None, + }; + Ok(RemoteWalletManager::new(hidapi, trezor_client)) } #[cfg(not(feature = "hidapi"))] pub fn initialize_wallet_manager() -> Result, RemoteWalletError> { @@ -323,7 +379,7 @@ pub fn initialize_wallet_manager() -> Result, RemoteWall pub fn maybe_wallet_manager() -> Result>, RemoteWalletError> { let wallet_manager = initialize_wallet_manager()?; let device_count = wallet_manager.update_devices()?; - if device_count > 0 { + if device_count > 0 || wallet_manager.has_trezor_client() { Ok(Some(wallet_manager)) } else { drop(wallet_manager); @@ -366,6 +422,35 @@ mod tests { pubkey: Pubkey::default(), error: None, })); + + let locator = Locator { + manufacturer: Manufacturer::Trezor, + pubkey: Some(pubkey), + }; + let wallet_info = RemoteWalletInfo::parse_locator(locator); + assert!(wallet_info.matches(&RemoteWalletInfo { + model: "T".to_string(), + manufacturer: Manufacturer::Trezor, + serial: "".to_string(), + host_device_path: "/host/device/path".to_string(), + pubkey, + error: None, + })); + + // Test that pubkey need not be populated + let locator = Locator { + manufacturer: Manufacturer::Trezor, + pubkey: None, + }; + let wallet_info = RemoteWalletInfo::parse_locator(locator); + assert!(wallet_info.matches(&RemoteWalletInfo { + model: "T".to_string(), + manufacturer: Manufacturer::Trezor, + serial: "".to_string(), + host_device_path: "/host/device/path".to_string(), + pubkey: Pubkey::default(), + error: None, + })); } #[test] @@ -397,6 +482,33 @@ mod tests { assert!(!info.matches(&test_info)); test_info.pubkey = pubkey; assert!(info.matches(&test_info)); + + let info = RemoteWalletInfo { + manufacturer: Manufacturer::Trezor, + model: "T".to_string(), + serial: "".to_string(), + host_device_path: "/host/device/path".to_string(), + pubkey, + error: None, + }; + let mut test_info = RemoteWalletInfo { + manufacturer: Manufacturer::Unknown, + ..RemoteWalletInfo::default() + }; + assert!(!info.matches(&test_info)); + test_info.manufacturer = Manufacturer::Trezor; + assert!(info.matches(&test_info)); + test_info.model = "Other".to_string(); + assert!(info.matches(&test_info)); + test_info.model = "T".to_string(); + assert!(info.matches(&test_info)); + test_info.host_device_path = "/host/device/path".to_string(); + assert!(info.matches(&test_info)); + let another_pubkey = solana_sdk::pubkey::new_rand(); + test_info.pubkey = another_pubkey; + assert!(!info.matches(&test_info)); + test_info.pubkey = pubkey; + assert!(info.matches(&test_info)); } #[test] @@ -415,5 +527,18 @@ mod tests { remote_wallet_info.get_pretty_path(), format!("usb://ledger/{pubkey_str}") ); + + let remote_wallet_info = RemoteWalletInfo { + model: "T".to_string(), + manufacturer: Manufacturer::Trezor, + serial: "".to_string(), + host_device_path: "/host/device/path".to_string(), + pubkey, + error: None, + }; + assert_eq!( + remote_wallet_info.get_pretty_path(), + format!("usb://trezor/{pubkey_str}") + ); } } diff --git a/remote-wallet/src/trezor.rs b/remote-wallet/src/trezor.rs new file mode 100644 index 00000000000000..f07f3650e6f233 --- /dev/null +++ b/remote-wallet/src/trezor.rs @@ -0,0 +1,227 @@ +use { + crate::remote_wallet::{ + RemoteWallet, RemoteWalletError, RemoteWalletInfo, RemoteWalletManager, + }, + bitcoin::bip32, + console::Emoji, + semver::Version as FirmwareVersion, + solana_sdk::{derivation_path::DerivationPath, pubkey::Pubkey, signature::Signature}, + std::{cell::RefCell, rc::Rc, str::FromStr, fmt}, + trezor_client::{ + client::common::handle_interaction, + protos::{ + SolanaGetPublicKey, + SolanaPublicKey, + SolanaSignTx, + SolanaTxSignature, + }, + Trezor, + utils::convert_path, + }, +}; + +static CHECK_MARK: Emoji = Emoji("✅ ", ""); + +/// Trezor Wallet device +pub struct TrezorWallet { + pub trezor_client: Option>>, + pub pretty_path: String, +} + +impl fmt::Debug for TrezorWallet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "trezor_client") + } +} + +impl TrezorWallet { + pub fn new(trezor_client: Option>>, pretty_path: String) -> Self { + Self { + trezor_client, + pretty_path, + } + } + + pub fn get_trezor_firmware_version(&self) -> Result { + match &self.trezor_client { + Some(trezor_client) => { + let features = trezor_client.borrow_mut().features().expect("no features").clone(); + Ok(FirmwareVersion::new(features.major_version().into(), features.minor_version().into(), features.patch_version().into())) + } + _ => Err(RemoteWalletError::NoDeviceFound) + } + } + + pub fn get_trezor_model(&self) -> Result { + match &self.trezor_client { + Some(trezor_client) => { + let features = trezor_client.borrow_mut().features().expect("no features").clone(); + Ok(features.model().to_string()) + } + _ => Err(RemoteWalletError::NoDeviceFound) + } + } + + pub fn get_trezor_device_id(&self) -> Result { + match &self.trezor_client { + Some(trezor_client) => { + let features = trezor_client.borrow_mut().features().expect("no features").clone(); + Ok(features.device_id().to_string()) + } + _ => Err(RemoteWalletError::NoDeviceFound) + } + } +} + +impl RemoteWallet for TrezorWallet { + fn name(&self) -> &str { + "Trezor hardware wallet" + } + + /// Parse device info and get device base pubkey + fn read_device(&mut self, _dev_info: &Trezor) -> Result { + unimplemented!(); + } + + /// Get solana pubkey from a RemoteWallet + fn get_pubkey( + &self, + derivation_path: &DerivationPath, + confirm_key: bool, + ) -> Result { + let derivation_path_string = format!("{:?}", derivation_path); + let derivation_path_str = derivation_path_string.as_str(); + match &self.trezor_client { + Some(trezor_client) => { + let mut solana_get_pubkey = SolanaGetPublicKey::new(); + let address_n = convert_path(&bip32::DerivationPath::from_str(derivation_path_str).expect("Should have properly formatted derivation path for converting")); + solana_get_pubkey.address_n = address_n; + solana_get_pubkey.show_display = Some(confirm_key); + if confirm_key { + println!("Waiting for your approval on {}", self.name()); + } + let pubkey = handle_interaction( + trezor_client.borrow_mut().call(solana_get_pubkey, Box::new(|_, m: SolanaPublicKey| Ok(m)))? + )?; + if confirm_key { + println!("{CHECK_MARK}Approved"); + } + Pubkey::try_from(pubkey.public_key()).map_err(|_| RemoteWalletError::Protocol("Key packet size mismatch")) + } + _ => Err(RemoteWalletError::NoDeviceFound) + } + } + + /// Sign transaction data with wallet managing pubkey at derivation path + /// `m/44'/501'/'/'`. + fn sign_message( + &self, + derivation_path: &DerivationPath, + data: &[u8], + ) -> Result { + let derivation_path_string = format!("{:?}", derivation_path); + let derivation_path_str = derivation_path_string.as_str(); + match &self.trezor_client { + Some(trezor_client) => { + let mut solana_sign_tx = SolanaSignTx::new(); + let address_n = convert_path(&bip32::DerivationPath::from_str(derivation_path_str).expect("Hardended Derivation Path")); + solana_sign_tx.address_n = address_n; + solana_sign_tx.serialized_tx = Some(data.to_vec()); + let solana_tx_signature = handle_interaction( + trezor_client.borrow_mut().call(solana_sign_tx, Box::new(|_, m: SolanaTxSignature| Ok(m)))? + )?; + Signature::try_from(solana_tx_signature.signature()).map_err(|_e| RemoteWalletError::Protocol("Signature packet size mismatch")) + } + _ => Err(RemoteWalletError::NoDeviceFound) + } + } + + /// Sign off-chain message with wallet managing pubkey at derivation path + /// `m/44'/501'/'/'`. + fn sign_offchain_message( + &self, + derivation_path: &DerivationPath, + message: &[u8], + ) -> Result { + Self::sign_message(&self, derivation_path, message) + } +} + +pub fn get_trezor_from_info(info: RemoteWalletInfo, _keypair_name: &str, wallet_manager: &RemoteWalletManager,) -> Result, RemoteWalletError> { + wallet_manager.get_trezor_wallet(info.get_pretty_path()) +} + +#[cfg(test)] +mod tests { + use serial_test::serial; + use trezor_client::{ + find_devices, + Model, + }; + + use super::*; + + fn init_emulator() -> Trezor { + let mut emulator = find_devices(false) + .into_iter() + .find(|t| t.model == Model::TrezorEmulator) + .expect("An emulator should be found") + .connect() + .expect("Connection to the emulator should succeed"); + emulator.init_device(None).expect("Initialization of device should succeed"); + emulator + } + + #[test] + #[serial] + fn test_emulator_find() { + let trezors = find_devices(false); + assert!(trezors.len() > 0); + assert!(trezors.iter().any(|t| t.model == Model::TrezorEmulator)); + } + + #[test] + #[serial] + fn test_solana_pubkey() { + let mut emulator = init_emulator(); + let derivation_path_str = "m/44'/501'/0'/0'"; + let mut solana_get_pubkey = SolanaGetPublicKey::new(); + let address_n = convert_path(&bip32::DerivationPath::from_str(derivation_path_str).expect("Failed to parse path")); + solana_get_pubkey.address_n = address_n; + solana_get_pubkey.show_display = Some(false); + let pubkey = handle_interaction( + emulator.call(solana_get_pubkey, Box::new(|_, m: SolanaPublicKey| Ok(m))).expect("Trezor client (the emulator) has been initialized and SolanaGetPublicKey is initialized correctly") + ).expect("Trezor client (the emulator) has been initialized and SolanaGetPublicKey is initialized correctly"); + let public_key_string = bitcoin::base58::encode(pubkey.public_key()); + assert!(!public_key_string.is_empty()); + } + + #[test] + #[serial] + fn test_trezor_wallet() { + let emulator = init_emulator(); + let pretty_path = "usb://trezor?key=0/0".to_string(); + let trezor_wallet = TrezorWallet::new(Some(Rc::new(RefCell::new(emulator))), pretty_path); + let expected_model = "T".to_string(); + let model = trezor_wallet.get_trezor_model().expect("Trezor client (the emulator) has been initialized"); + assert_eq!(expected_model, model); + let device_id = trezor_wallet.get_trezor_device_id().expect("Trezor client (the emulator) has been initialized"); + assert!(!device_id.is_empty()); + let firmware_version = trezor_wallet.get_trezor_firmware_version(); + assert!(firmware_version.is_ok()); + let derivation_path = DerivationPath::new_bip44(Some(0), Some(0)); + let pubkey = trezor_wallet.get_pubkey(&derivation_path, false).expect("Trezor client (the emulator) has been initialized"); + assert!(!pubkey.to_string().is_empty()); + } + + #[test] + #[serial] + fn test_trezor_wallet_with_trezor_client_none() { + let pretty_path = "usb://trezor?key=0/0".to_string(); + let trezor_wallet = TrezorWallet::new(None, pretty_path); + let derivation_path = DerivationPath::new_bip44(Some(0), Some(0)); + let res = trezor_wallet.get_pubkey(&derivation_path, false); + assert!(res.is_err()); + assert_eq!(res.unwrap_err().to_string(), RemoteWalletError::NoDeviceFound.to_string()); + } +}