diff --git a/.cargo/config.toml b/.cargo/config.toml index c233973..aee4a63 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -5,5 +5,5 @@ runner = "speculos -a=1 --model=nanosp" target = "nanosplus" [unstable] -build-std = ["core"] +build-std = ["core", "alloc"] build-std-features = ["compiler-builtins-mem"] diff --git a/Cargo.lock b/Cargo.lock index 5589baf..1656a64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "app-boilerplate-rust" -version = "1.3.0" +version = "1.4.0" dependencies = [ "hex", "include_gif", @@ -99,9 +99,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "clang-sys" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a483f3cbf7cec2e153d424d0e92329d816becc6421389bd494375c6065921b9b" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", @@ -129,12 +129,28 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" + [[package]] name = "either" version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +[[package]] +name = "embedded-alloc" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddae17915accbac2cfbc64ea0ae6e3b330e6ea124ba108dada63646fd3c6f815" +dependencies = [ + "critical-section", + "linked_list_allocator", +] + [[package]] name = "errno" version = "0.3.9" @@ -233,9 +249,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "ledger_device_sdk" -version = "1.9.2" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c799dd808a566399aa22a31e3fa9563c8f9959a4960e3fbef65a7b492f9f83f7" +checksum = "8e719833effcba96fae21d5b1afeb82f07d599ab35dbd92bf427851f2c90d459" dependencies = [ "const-zero", "include_gif", @@ -248,12 +264,14 @@ dependencies = [ [[package]] name = "ledger_secure_sdk_sys" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c421b2537a2181fa5a48d48529e9c5b55c494bc0d114c0be0d12c3783a4c9382" +checksum = "c72e7d079586787e1398593532f57e02a86c56208aeb030a4aef8f2fbd2b8d2e" dependencies = [ "bindgen", "cc", + "critical-section", + "embedded-alloc", "glob", ] @@ -273,6 +291,12 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "linked_list_allocator" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afa463f5405ee81cdb9cc2baf37e08ec7e4c8209442b5d72c04cfb2cd6e6286" + [[package]] name = "linux-raw-sys" version = "0.4.14" @@ -355,9 +379,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.84" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] diff --git a/Cargo.toml b/Cargo.toml index 629c73d..c2f1d10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,15 @@ [package] name = "app-boilerplate-rust" -version = "1.3.0" +version = "1.4.0" authors = ["yhql", "agrojean-ledger"] edition = "2021" [dependencies] -ledger_device_sdk = "1.9.2" +ledger_device_sdk = { version="1.10.1", features = ["heap"] } include_gif = "1.1.0" serde = {version="1.0.192", default_features = false, features = ["derive"]} serde-json-core = { git = "https://github.com/rust-embedded-community/serde-json-core"} -hex = { version = "0.4.3", default-features = false, features = ["serde"] } +hex = { version = "0.4.3", default-features = false, features = ["serde", "alloc"] } numtoa = "0.2.4" [profile.release] @@ -26,9 +26,6 @@ flags = "0" path = ["44'/1'"] name = "Rust Boilerplate" -[package.metadata.ledger.nanos] -icon = "crab.gif" - [package.metadata.ledger.nanox] icon = "crab_14x14.gif" @@ -39,4 +36,4 @@ icon = "crab_14x14.gif" icon = "crab_32x32.gif" [package.metadata.ledger.flex] -icon = "crab_40x40.gif" +icon = "crab_40x40.gif" \ No newline at end of file diff --git a/README.md b/README.md index 2681888..0c9e365 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ ![Rule enforcer](https://github.com/LedgerHQ/app-boilerplate-rust/actions/workflows/guidelines_enforcer.yml/badge.svg) ![Build and tests](https://github.com/LedgerHQ/app-boilerplate-rust/actions/workflows/build_and_functional_tests.yml/badge.svg) -This is a boilerplate application written in Rust which can be forked to start a new project for the Ledger Nano S/X/SP, Stax and Flex devices. +This is a boilerplate application written in Rust which can be forked to start a new project for the Ledger Nano X/SP, Stax and Flex devices. + +:warning: Nano S is not supported * Implements standard features (display address, transaction signature...), * Has functional tests using [Ragger](https://github.com/LedgerHQ/ragger), @@ -82,7 +84,7 @@ Now that you have followed the [prerequisites](#prerequisites) guide, you can bu cargo ledger build nanox ``` -This command will build the app for the Nano X, but you can use any supported device (`nanos`, `nanox`, `nanosplus`) +This command will build the app for the Nano X, but you can use any supported device (`nanox`, `nanosplus`, `stax`, `flex`) ### Loading @@ -94,7 +96,7 @@ This command will build the app for the Nano X, but you can use any supported de cargo ledger build nanox --load ``` -As for the build command, you can replace `nanos` with `nanox` or `nanosplus`. +As for the build command, you can replace `nanox` with `nanosplus`, `stax` or `flex`. ## Test @@ -111,7 +113,7 @@ pip install -r tests/requirements.txt * Run the functional tests : ```shell -pytest tests/ --tb=short -v --device {nanos | nanosp | nanox} +pytest tests/ --tb=short -v --device {nanosp | nanox | stax | flex} ``` ### Emulator diff --git a/ledger_app.toml b/ledger_app.toml index 5266444..cb24deb 100644 --- a/ledger_app.toml +++ b/ledger_app.toml @@ -1,7 +1,7 @@ [app] build_directory = "./" sdk = "Rust" -devices = ["nanos", "nanox", "nanos+", "stax", "flex"] +devices = ["nanox", "nanos+", "stax", "flex"] [tests] pytest_directory = "./tests/" diff --git a/src/app_ui/address.rs b/src/app_ui/address.rs index 1c2ff63..176bb18 100644 --- a/src/app_ui/address.rs +++ b/src/app_ui/address.rs @@ -16,7 +16,7 @@ *****************************************************************************/ use crate::AppSW; -use core::str::from_utf8_mut; +use alloc::format; #[cfg(not(any(target_os = "stax", target_os = "flex")))] use ledger_device_sdk::ui::{ @@ -34,21 +34,16 @@ use include_gif::include_gif; const DISPLAY_ADDR_BYTES_LEN: usize = 20; pub fn ui_display_pk(addr: &[u8]) -> Result { - let mut addr_hex = [0u8; DISPLAY_ADDR_BYTES_LEN * 2 + 2]; - addr_hex[..2].copy_from_slice("0x".as_bytes()); - hex::encode_to_slice( - &addr[addr.len() - DISPLAY_ADDR_BYTES_LEN..], - &mut addr_hex[2..], - ) - .unwrap(); - let addr_hex = from_utf8_mut(&mut addr_hex).unwrap(); - addr_hex[2..].make_ascii_uppercase(); + let addr_hex = format!( + "0x{}", + hex::encode(&addr[addr.len() - DISPLAY_ADDR_BYTES_LEN..]).to_uppercase() + ); #[cfg(not(any(target_os = "stax", target_os = "flex")))] { let my_field = [Field { name: "Address", - value: addr_hex, + value: addr_hex.as_str(), }]; let my_review = MultiFieldReview::new( @@ -72,6 +67,6 @@ pub fn ui_display_pk(addr: &[u8]) -> Result { Ok(NbglAddressReview::new() .glyph(&FERRIS) .verify_str("Verify CRAB address") - .show(addr_hex)) + .show(&addr_hex)) } } diff --git a/src/app_ui/sign.rs b/src/app_ui/sign.rs index abefde1..d888d7c 100644 --- a/src/app_ui/sign.rs +++ b/src/app_ui/sign.rs @@ -15,7 +15,6 @@ * limitations under the License. *****************************************************************************/ use crate::handlers::sign_tx::Tx; -use crate::utils::concatenate; use crate::AppSW; #[cfg(not(any(target_os = "stax", target_os = "flex")))] @@ -31,9 +30,7 @@ use include_gif::include_gif; #[cfg(any(target_os = "stax", target_os = "flex"))] use ledger_device_sdk::nbgl::{Field, NbglGlyph, NbglReview}; -use numtoa::NumToA; - -const MAX_COIN_LENGTH: usize = 10; +use alloc::format; /// Displays a transaction and returns true if user approved it. /// @@ -43,31 +40,18 @@ const MAX_COIN_LENGTH: usize = 10; /// /// * `tx` - Transaction to be displayed for validation pub fn ui_display_tx(tx: &Tx) -> Result { - // Generate string for amount - let mut numtoa_buf = [0u8; 20]; - let mut value_buf = [0u8; 20 + MAX_COIN_LENGTH + 1]; - - let value_str = concatenate( - &[tx.coin, " ", tx.value.numtoa_str(10, &mut numtoa_buf)], - &mut value_buf, - ) - .map_err(|_| AppSW::TxDisplayFail)?; // Fails if value_buf is too small - - // Generate destination address string in hexadecimal format. - let mut to_str = [0u8; 42]; - to_str[..2].copy_from_slice("0x".as_bytes()); - hex::encode_to_slice(tx.to, &mut to_str[2..]).unwrap(); - to_str[2..].make_ascii_uppercase(); + let value_str = format!("{} {}", tx.coin, tx.value); + let to_str = format!("0x{}", hex::encode(tx.to).to_uppercase()); // Define transaction review fields let my_fields = [ Field { name: "Amount", - value: value_str, + value: value_str.as_str(), }, Field { name: "Destination", - value: core::str::from_utf8(&to_str).unwrap(), + value: to_str.as_str(), }, Field { name: "Memo", diff --git a/src/handlers/sign_tx.rs b/src/handlers/sign_tx.rs index 5a98a76..8b6ddee 100644 --- a/src/handlers/sign_tx.rs +++ b/src/handlers/sign_tx.rs @@ -17,6 +17,7 @@ use crate::app_ui::sign::ui_display_tx; use crate::utils::Bip32Path; use crate::AppSW; +use alloc::vec::Vec; use ledger_device_sdk::ecc::{Secp256k1, SeedDerive}; use ledger_device_sdk::hash::{sha3::Keccak256, HashInit}; use ledger_device_sdk::io::Comm; @@ -38,8 +39,7 @@ pub struct Tx<'a> { } pub struct TxContext { - raw_tx: [u8; MAX_TRANSACTION_LEN], // raw transaction serialized - raw_tx_len: usize, // length of raw transaction + raw_tx: Vec, path: Bip32Path, } @@ -47,15 +47,13 @@ pub struct TxContext { impl TxContext { pub fn new() -> TxContext { TxContext { - raw_tx: [0u8; MAX_TRANSACTION_LEN], - raw_tx_len: 0, + raw_tx: Vec::new(), path: Default::default(), } } // Implement reset for TxInfo fn reset(&mut self) { - self.raw_tx = [0u8; MAX_TRANSACTION_LEN]; - self.raw_tx_len = 0; + self.raw_tx.clear(); self.path = Default::default(); } } @@ -78,13 +76,12 @@ pub fn handler_sign_tx( // Next chunks, append data to raw_tx and return or parse // the transaction if it is the last chunk. } else { - if ctx.raw_tx_len + data.len() > MAX_TRANSACTION_LEN { + if ctx.raw_tx.len() + data.len() > MAX_TRANSACTION_LEN { return Err(AppSW::TxWrongLength); } // Append data to raw_tx - ctx.raw_tx[ctx.raw_tx_len..ctx.raw_tx_len + data.len()].copy_from_slice(data); - ctx.raw_tx_len += data.len(); + ctx.raw_tx.extend(data); // If we expect more chunks, return if more { @@ -92,8 +89,7 @@ pub fn handler_sign_tx( // Otherwise, try to parse the transaction } else { // Try to deserialize the transaction - let (tx, _): (Tx, usize) = - from_slice(&ctx.raw_tx[..ctx.raw_tx_len]).map_err(|_| AppSW::TxParsingFail)?; + let (tx, _): (Tx, usize) = from_slice(&ctx.raw_tx).map_err(|_| AppSW::TxParsingFail)?; // Display transaction. If user approves // the transaction, sign it. Otherwise, // return a "deny" status word. @@ -110,7 +106,7 @@ fn compute_signature_and_append(comm: &mut Comm, ctx: &mut TxContext) -> Result< let mut keccak256 = Keccak256::new(); let mut message_hash: [u8; 32] = [0u8; 32]; - let _ = keccak256.hash(&ctx.raw_tx[..ctx.raw_tx_len], &mut message_hash); + let _ = keccak256.hash(&ctx.raw_tx, &mut message_hash); let (sig, siglen, parity) = Secp256k1::derive_from_path(ctx.path.as_ref()) .deterministic_sign(&message_hash) diff --git a/src/main.rs b/src/main.rs index 5445208..94c537e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,9 +42,12 @@ use ledger_device_sdk::io::{ApduHeader, Comm, Event, Reply, StatusWords}; #[cfg(feature = "pending_review_screen")] #[cfg(not(any(target_os = "stax", target_os = "flex")))] use ledger_device_sdk::ui::gadgets::display_pending_review; -#[cfg(not(any(target_os = "stax", target_os = "flex")))] + ledger_device_sdk::set_panic!(ledger_device_sdk::exiting_panic); +// Required for using String, Vec, format!... +extern crate alloc; + #[cfg(any(target_os = "stax", target_os = "flex"))] use ledger_device_sdk::nbgl::init_comm; diff --git a/src/utils.rs b/src/utils.rs index 77448e7..bac3ea0 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,32 +1,18 @@ +use alloc::vec::Vec; + use crate::AppSW; -use core::str::from_utf8; /// BIP32 path stored as an array of [`u32`]. -/// -/// # Generic arguments -/// -/// * `S` - Maximum possible path length, i.e. the capacity of the internal buffer. -pub struct Bip32Path { - buffer: [u32; S], - len: usize, -} +#[derive(Default)] +pub struct Bip32Path(Vec); impl AsRef<[u32]> for Bip32Path { fn as_ref(&self) -> &[u32] { - &self.buffer[..self.len] - } -} - -impl Default for Bip32Path { - fn default() -> Self { - Self { - buffer: [0u32; S], - len: 0, - } + &self.0 } } -impl TryFrom<&[u8]> for Bip32Path { +impl TryFrom<&[u8]> for Bip32Path { type Error = AppSW; /// Constructs a [`Bip32Path`] from a given byte array. @@ -34,46 +20,23 @@ impl TryFrom<&[u8]> for Bip32Path { /// This method will return an error in the following cases: /// - the input array is empty, /// - the number of bytes in the input array is not a multiple of 4, - /// - the input array exceeds the capacity of the [`Bip32Path`] internal buffer. /// /// # Arguments /// /// * `data` - Encoded BIP32 path. First byte is the length of the path, as encoded by ragger. fn try_from(data: &[u8]) -> Result { - let input_path_len = (data.len() - 1) / 4; // Check data length if data.is_empty() // At least the length byte is required - || (input_path_len > S) || (data[0] as usize * 4 != data.len() - 1) { return Err(AppSW::WrongApduLength); } - let mut path = [0; S]; - for (chunk, p) in data[1..].chunks(4).zip(path.iter_mut()) { - *p = u32::from_be_bytes(chunk.try_into().unwrap()); - } - - Ok(Self { - buffer: path, - len: input_path_len, - }) + Ok(Bip32Path( + data[1..] + .chunks(4) + .map(|chunk| u32::from_be_bytes(chunk.try_into().unwrap())) + .collect(), + )) } } - -/// Returns concatenated strings, or an error if the concatenation buffer is too small. -pub fn concatenate<'a>(strings: &[&str], output: &'a mut [u8]) -> Result<&'a str, ()> { - let mut offset = 0; - - for s in strings { - let s_len = s.len(); - if offset + s_len > output.len() { - return Err(()); - } - - output[offset..offset + s_len].copy_from_slice(s.as_bytes()); - offset += s_len; - } - - Ok(from_utf8(&output[..offset]).unwrap()) -} diff --git a/tests/snapshots/flex/test_app_mainmenu/00002.png b/tests/snapshots/flex/test_app_mainmenu/00002.png index 4f4e59c..1c35dc9 100644 Binary files a/tests/snapshots/flex/test_app_mainmenu/00002.png and b/tests/snapshots/flex/test_app_mainmenu/00002.png differ diff --git a/tests/snapshots/nanosp/test_app_mainmenu/00001.png b/tests/snapshots/nanosp/test_app_mainmenu/00001.png index 2cdf2d1..ade0ca0 100644 Binary files a/tests/snapshots/nanosp/test_app_mainmenu/00001.png and b/tests/snapshots/nanosp/test_app_mainmenu/00001.png differ diff --git a/tests/snapshots/nanox/test_app_mainmenu/00001.png b/tests/snapshots/nanox/test_app_mainmenu/00001.png index 2cdf2d1..ade0ca0 100644 Binary files a/tests/snapshots/nanox/test_app_mainmenu/00001.png and b/tests/snapshots/nanox/test_app_mainmenu/00001.png differ diff --git a/tests/snapshots/stax/test_app_mainmenu/00002.png b/tests/snapshots/stax/test_app_mainmenu/00002.png index 7317fdb..eb9eccc 100644 Binary files a/tests/snapshots/stax/test_app_mainmenu/00002.png and b/tests/snapshots/stax/test_app_mainmenu/00002.png differ