From 04933268333a56c22e98fbc0e5f5f85c184ca9ff Mon Sep 17 00:00:00 2001 From: GroM Date: Fri, 8 Dec 2023 10:55:40 +0100 Subject: [PATCH 1/6] added cargo-ledger package --- Cargo.toml | 3 +- README.md | 3 +- cargo-ledger/.rustfmt.toml | 1 + cargo-ledger/Cargo.toml | 18 + cargo-ledger/LICENSE.md | 201 ++++++++++ cargo-ledger/README.md | 57 +++ cargo-ledger/src/main.rs | 373 +++++++++++++++++++ cargo-ledger/src/setup.rs | 42 +++ cargo-ledger/src/utils.rs | 128 +++++++ cargo-ledger/tests/valid/Cargo.toml | 22 ++ cargo-ledger/tests/valid_outdated/Cargo.toml | 16 + cargo-ledger/tests/valid_variant/Cargo.toml | 16 + 12 files changed, 878 insertions(+), 2 deletions(-) create mode 100644 cargo-ledger/.rustfmt.toml create mode 100644 cargo-ledger/Cargo.toml create mode 100644 cargo-ledger/LICENSE.md create mode 100644 cargo-ledger/README.md create mode 100644 cargo-ledger/src/main.rs create mode 100644 cargo-ledger/src/setup.rs create mode 100644 cargo-ledger/src/utils.rs create mode 100644 cargo-ledger/tests/valid/Cargo.toml create mode 100644 cargo-ledger/tests/valid_outdated/Cargo.toml create mode 100644 cargo-ledger/tests/valid_variant/Cargo.toml diff --git a/Cargo.toml b/Cargo.toml index 5edd93bb..051e7679 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,8 @@ members = [ "ledger_device_sdk", "ledger_secure_sdk_sys", "include_gif", - "testmacro" + "testmacro", + "cargo-ledger" ] resolver = "2" diff --git a/README.md b/README.md index b352e454..e6803a1a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Ledger Device Rust SDK -This workspace contains the 4 crates members of Ledger Device Rust SDK +This workspace contains the 5 crates members of Ledger Device Rust SDK * [ledger_device_sdk](./ledger_device_sdk): main Rust SDK crate used to build an application that runs on BOLOS OS, * [ledger_secure_sdk_sys](./ledger_secure_sdk_sys): bindings to [ledger_secure_sdk](https://github.com/LedgerHQ/ledger-secure-sdk) * [include_gif](./include_gif): procedural macro used to manage GIF * [testmacro](./testmacro): procedural macro used by unit and integrations tests +* [cargo-ledger](./cargo_ledger): tool to build Ledger device applications developped in Rust diff --git a/cargo-ledger/.rustfmt.toml b/cargo-ledger/.rustfmt.toml new file mode 100644 index 00000000..df99c691 --- /dev/null +++ b/cargo-ledger/.rustfmt.toml @@ -0,0 +1 @@ +max_width = 80 diff --git a/cargo-ledger/Cargo.toml b/cargo-ledger/Cargo.toml new file mode 100644 index 00000000..1769d875 --- /dev/null +++ b/cargo-ledger/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cargo-ledger" +version = "1.2.0" +authors = ["yhql"] +description = "Build and sideload Ledger Nano apps" +categories = ["development-tools::cargo-plugins"] +repository = "https://github.com/LedgerHQ/cargo-ledger" +readme = "README.md" +license = "Apache-2.0" +edition = "2018" + +[dependencies] +cargo_metadata = "0.11.0" +clap = { version = "4.1.8", features = ["derive"] } +goblin = "0.2.3" +serde = "1.0" +serde_derive = "1.0" +serde_json = "1.0" diff --git a/cargo-ledger/LICENSE.md b/cargo-ledger/LICENSE.md new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/cargo-ledger/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/cargo-ledger/README.md b/cargo-ledger/README.md new file mode 100644 index 00000000..b5996d39 --- /dev/null +++ b/cargo-ledger/README.md @@ -0,0 +1,57 @@ +# Cargo-ledger + +Builds a Nano App and outputs a JSON manifest file that can be used by [ledgerctl](https://github.com/LedgerHQ/ledgerctl) to install an application directly. + +In order to build for Nano S, Nano X, and Nano S Plus, [custom target files](https://docs.rust-embedded.org/embedonomicon/custom-target.html) are used. They can be found at the root of the [Rust SDK](https://github.com/LedgerHQ/ledger-nanos-sdk/) and can be installed automatically with the command `setup`. + +## Installation + +This program requires: + +- `arm-none-eabi-objcopy` +- [`ledgerctl`](https://github.com/LedgerHQ/ledgerctl) + +Install this repo with: + +``` +cargo install --git https://github.com/LedgerHQ/cargo-ledger +``` + +or download it manually and install with: + +``` +cargo install --path . +``` + +Note that `cargo`'s dependency resolver may behave differently when installing, and you may end up with errors. +In order to fix those and force usage of the versions specified in the tagged `Cargo.lock`, append `--locked` to the above commands. + +## Usage + +General usage is displayed when invoking `cargo ledger`. + +### Setup + +This will install custom target files from the SDK directly into your environment. + +``` +cargo ledger setup +``` + +### Building + +``` +cargo ledger build nanos +cargo ledger build nanox +cargo ledger build nanosplus +``` + +Loading on device can optionally be performed by appending `--load` or `-l` to the command. + +By default, this program will attempt to build the current program with in `release` mode (full command: `cargo build --release --target=nanos --message-format=json`) + +Arguments can be passed to modify this behaviour after inserting a `--` like so: + +``` +cargo ledger build nanos --load -- --features one -Z unstable-options --out-dir ./output/ +``` \ No newline at end of file diff --git a/cargo-ledger/src/main.rs b/cargo-ledger/src/main.rs new file mode 100644 index 00000000..12d31982 --- /dev/null +++ b/cargo-ledger/src/main.rs @@ -0,0 +1,373 @@ +use std::fmt::{Display, Formatter}; +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use std::process::Stdio; + +use cargo_metadata::{Message, Package}; +use clap::{Parser, Subcommand, ValueEnum}; +use serde_derive::Deserialize; +use serde_json::json; + +use setup::install_targets; +use utils::*; + +mod setup; +mod utils; + +/// Structure for retrocompatibility, when the cargo manifest file +/// contains a single `[package.metadata.nanos]` section +#[derive(Debug, Deserialize)] +struct NanosMetadata { + curve: Vec, + path: Vec, + flags: String, + icon: String, + icon_small: String, + name: Option, +} + +#[derive(Debug, Deserialize)] +struct LedgerMetadata { + curve: Vec, + path: Vec, + flags: String, + name: Option, +} + +#[derive(Debug, Deserialize)] +struct DeviceMetadata { + icon: String, +} + +#[derive(Parser, Debug)] +#[command(name = "cargo")] +#[command(bin_name = "cargo")] +#[clap(name = "Ledger devices build and load commands")] +#[clap(version = "0.0")] +#[clap(about = "Builds the project and emits a JSON manifest for ledgerctl.")] +enum Cli { + Ledger(CliArgs), +} + +#[derive(clap::Args, Debug)] +struct CliArgs { + #[clap(long)] + #[clap(value_name = "prebuilt ELF exe")] + use_prebuilt: Option, + + #[clap(long)] + #[clap(help = concat!( + "Should the app.hex be placed next to the app.json, or next to the input exe?", + " ", + "Typically used with --use-prebuilt when the input exe is in a read-only location.", + ))] + hex_next_to_json: bool, + + #[clap(subcommand)] + command: MainCommand, +} + +#[derive(ValueEnum, Clone, Copy, Debug, PartialEq)] +enum Device { + Nanos, + Nanox, + Nanosplus, +} + +impl Display for Device { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_ref()) + } +} + +impl AsRef for Device { + fn as_ref(&self) -> &str { + match self { + Device::Nanos => "nanos", + Device::Nanox => "nanox", + Device::Nanosplus => "nanosplus", + } + } +} + +#[derive(Subcommand, Debug)] +enum MainCommand { + #[clap(about = "install custom target files")] + Setup, + #[clap(about = "build the project for a given device")] + Build { + #[clap(value_enum)] + #[clap(help = "device to build for")] + device: Device, + #[clap(short, long)] + #[clap(help = "load on a device")] + load: bool, + #[clap(last = true)] + remaining_args: Vec, + }, +} + +fn main() { + let Cli::Ledger(cli) = Cli::parse(); + + match cli.command { + MainCommand::Setup => install_targets(), + MainCommand::Build { + device: d, + load: a, + remaining_args: r, + } => { + build_app(d, a, cli.use_prebuilt, cli.hex_next_to_json, r); + } + } +} + +fn retrieve_metadata( + device: Device, + manifest_path: Option<&str>, +) -> (Package, LedgerMetadata, DeviceMetadata) { + let mut cmd = cargo_metadata::MetadataCommand::new(); + + // Only used during tests + if let Some(manifestpath) = manifest_path { + cmd = cmd.manifest_path(manifestpath).clone(); + } + + let res = cmd + .no_deps() + .exec() + .expect("Could not execute `cargo metadata`"); + + let this_pkg = res.packages.last().unwrap(); + let metadata_section = this_pkg.metadata.get("ledger"); + + if let Some(metadatasection) = metadata_section { + let metadata_device = metadata_section + .unwrap() + .clone() + .get(device.as_ref()) + .unwrap() + .clone(); + + let ledger_metadata: LedgerMetadata = + serde_json::from_value(metadatasection.clone()) + .expect("Could not deserialize medatada.ledger"); + let device_metadata: DeviceMetadata = + serde_json::from_value(metadata_device) + .expect("Could not deserialize device medatada"); + + (this_pkg.clone(), ledger_metadata, device_metadata) + } else { + println!("WARNING: 'package.metadata.ledger' section is missing in Cargo.toml, trying 'package.metadata.nanos'"); + let nanos_section = this_pkg.metadata.get("nanos").expect( + "No appropriate [package.metadata.] section found.", + ); + + let nanos_metadata: NanosMetadata = + serde_json::from_value(nanos_section.clone()) + .expect("Could not deserialize medatada.nanos"); + let ledger_metadata = LedgerMetadata { + curve: nanos_metadata.curve, + path: nanos_metadata.path, + flags: nanos_metadata.flags, + name: nanos_metadata.name, + }; + + let device_metadata = DeviceMetadata { + icon: match device { + Device::Nanos => nanos_metadata.icon, + _ => nanos_metadata.icon_small, + }, + }; + + (this_pkg.clone(), ledger_metadata, device_metadata) + } +} + +fn build_app( + device: Device, + is_load: bool, + use_prebuilt: Option, + hex_next_to_json: bool, + remaining_args: Vec, +) { + let exe_path = match use_prebuilt { + None => { + let mut cargo_cmd = Command::new("cargo") + .args([ + "build", + "--release", + format!("--target={}", device.as_ref()).as_str(), + "--message-format=json-diagnostic-rendered-ansi", + ]) + .args(&remaining_args) + .stdout(Stdio::piped()) + .spawn() + .unwrap(); + + let mut exe_path = PathBuf::new(); + let out = cargo_cmd.stdout.take().unwrap(); + let reader = std::io::BufReader::new(out); + for message in Message::parse_stream(reader) { + match message.as_ref().unwrap() { + Message::CompilerArtifact(artifact) => { + if let Some(n) = &artifact.executable { + exe_path = n.to_path_buf(); + } + } + Message::CompilerMessage(message) => { + println!("{message}"); + } + _ => (), + } + } + + cargo_cmd.wait().expect("Couldn't get cargo's exit status"); + + exe_path + } + Some(prebuilt) => prebuilt.canonicalize().unwrap(), + }; + + let (this_pkg, metadata_ledger, metadata_device) = + retrieve_metadata(device, None); + let current_dir = this_pkg + .manifest_path + .parent() + .expect("Could not find package's parent path"); + + let hex_file_abs = if hex_next_to_json { + current_dir + } else { + exe_path.parent().unwrap() + } + .join("app.hex"); + + export_binary(&exe_path, &hex_file_abs); + + // app.json will be placed in the app's root directory + let app_json_name = format!("app_{}.json", device.as_ref()); + let app_json = current_dir.join(app_json_name); + + // Find hex file path relative to 'app.json' + let hex_file = hex_file_abs.strip_prefix(current_dir).unwrap(); + + // Retrieve real data size and SDK infos from ELF + let infos = retrieve_infos(&exe_path).unwrap(); + + // Modify flags to enable BLE if targeting Nano X + let flags = match device { + Device::Nanos | Device::Nanosplus => metadata_ledger.flags, + Device::Nanox => { + let base = u32::from_str_radix(metadata_ledger.flags.as_str(), 16) + .unwrap_or(0); + format!("0x{:x}", base | 0x200) + } + }; + + // Target ID according to target, in case it + // is not present in the retrieved ELF infos. + let backup_targetid : String = match device { + Device::Nanos => String::from("0x31100004"), + Device::Nanox => String::from("0x33000004"), + Device::Nanosplus => String::from("0x33100004"), + }; + + // create manifest + let file = fs::File::create(&app_json).unwrap(); + let mut json = json!({ + "name": metadata_ledger.name.as_ref().unwrap_or(&this_pkg.name), + "version": &this_pkg.version, + "icon": metadata_device.icon, + "targetId": infos.target_id.unwrap_or(backup_targetid), + "flags": flags, + "derivationPath": { + "curves": metadata_ledger.curve, + "paths": metadata_ledger.path + }, + "binary": hex_file, + "dataSize": infos.size + }); + // Ignore apiLevel for Nano S as it is unsupported for now + match device { + Device::Nanos => (), + _ => { + json["apiLevel"] = infos.api_level.into(); + } + } + serde_json::to_writer_pretty(file, &json).unwrap(); + + // Use ledgerctl to dump the APDU installation file. + // Either dump to the location provided by the --out-dir cargo + // argument if provided or use the default binary path. + let output_dir: Option = remaining_args + .iter() + .position(|arg| arg == "--out-dir" || arg.starts_with("--out-dir=")) + .and_then(|index| { + let out_dir_arg = &remaining_args[index]; + // Extracting the value from "--out-dir=" or "--out-dir " + if out_dir_arg.contains('=') { + Some(out_dir_arg.split('=').nth(1).unwrap().to_string()) + } else { + remaining_args + .get(index + 1) + .map(|path_str| path_str.to_string()) + } + }) + .map(|path_str| PathBuf::from(path_str)); + let exe_filename = exe_path.file_name().unwrap().to_str(); + let exe_parent = exe_path.parent().unwrap().to_path_buf(); + let apdu_file_path = output_dir.unwrap_or(exe_parent).join(exe_filename.unwrap()).with_extension("apdu"); + dump_with_ledgerctl(current_dir, &app_json, apdu_file_path.to_str().unwrap()); + + if is_load { + install_with_ledgerctl(current_dir, &app_json); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn valid_metadata() { + let (_, metadata_ledger, metadata_nanos) = + retrieve_metadata(Device::Nanos, Some("./tests/valid/Cargo.toml")); + + assert_eq!(metadata_ledger.name, Some("TestApp".to_string())); + assert_eq!(metadata_ledger.curve, ["secp256k1"]); + assert_eq!(metadata_ledger.flags, "0x38"); + assert_eq!(metadata_ledger.path, ["'44/123"]); + + assert_eq!(metadata_nanos.icon, "./assets/nanos.gif") + } + + #[test] + fn valid_metadata_variant() { + let (_, metadata_ledger, metadata_nanos) = retrieve_metadata( + Device::Nanos, + Some("./tests/valid_variant/Cargo.toml"), + ); + + assert_eq!(metadata_ledger.name, Some("TestApp".to_string())); + assert_eq!(metadata_ledger.curve, ["secp256k1"]); + assert_eq!(metadata_ledger.flags, "0x38"); + assert_eq!(metadata_ledger.path, ["'44/123"]); + assert_eq!(metadata_nanos.icon, "./assets/nanos.gif") + } + + #[test] + fn valid_outdated_metadata() { + let (_, metadata_ledger, metadata_nanos) = retrieve_metadata( + Device::Nanos, + Some("./tests/valid_outdated/Cargo.toml"), + ); + + assert_eq!(metadata_ledger.name, Some("TestApp".to_string())); + assert_eq!(metadata_ledger.curve, ["secp256k1"]); + assert_eq!(metadata_ledger.flags, "0"); + assert_eq!(metadata_ledger.path, ["'44/123"]); + assert_eq!(metadata_nanos.icon, "nanos.gif") + } +} diff --git a/cargo-ledger/src/setup.rs b/cargo-ledger/src/setup.rs new file mode 100644 index 00000000..1e5d7c2f --- /dev/null +++ b/cargo-ledger/src/setup.rs @@ -0,0 +1,42 @@ +use std::path::Path; +use std::process::Command; + +pub fn install_targets() { + println!("[ ] Checking for installed custom targets..."); + // Check if target files are installed + let sysroot_cmd = Command::new("rustc") + .arg("--print") + .arg("sysroot") + .output() + .expect("failed to call rustc") + .stdout; + let sysroot_cmd = std::str::from_utf8(&sysroot_cmd).unwrap().trim(); + + let target_files_url = Path::new( + "https://raw.githubusercontent.com/LedgerHQ/ledger-device-rust-sdk/cee5644d6c20ff97b13e79a30caca751b7b52ac8/ledger_device_sdk/", + ); + let sysroot = Path::new(sysroot_cmd).join("lib").join("rustlib"); + + // Retrieve each target file independently + // TODO: handle target.json modified upstream + for target in &["nanos", "nanox", "nanosplus"] { + let outfilepath = sysroot.join(target).join("target.json"); + if !outfilepath.exists() { + let targetpath = + outfilepath.clone().into_os_string().into_string().unwrap(); + println!("* Adding \x1b[1;32m{target}\x1b[0m in \x1b[1;33m{targetpath}\x1b[0m"); + + let target_url = target_files_url.join(format!("{target}.json")); + let cmd = Command::new("curl") + .arg(target_url) + .arg("-o") + .arg(outfilepath) + .arg("--create-dirs") + .output() + .expect("failed to execute 'curl'"); + println!("{}", std::str::from_utf8(&cmd.stderr).unwrap()); + } else { + println!("* {target} already installed"); + } + } +} diff --git a/cargo-ledger/src/utils.rs b/cargo-ledger/src/utils.rs new file mode 100644 index 00000000..ca279fdc --- /dev/null +++ b/cargo-ledger/src/utils.rs @@ -0,0 +1,128 @@ +use std::env; +use std::fs; +use std::io; +use std::io::Write; +use std::process::Command; + +#[derive(Default, Debug)] +pub struct LedgerAppInfos { + pub api_level: String, + pub target_id : Option, + pub size: u64, +} + +fn get_string_from_offset(vector: &Vec,offset: &usize) -> String { + // Find the end of the string (search for a line feed character) + let end_index = vector[*offset..] + .iter() + .position(|&x| x == '\n' as u8) + .map(|pos| *offset + pos) + .unwrap_or(*offset); // Use the start offset if the delimiter position is not found + String::from_utf8(vector[*offset..end_index].to_vec()).expect("Invalid UTF-8") +} + +pub fn retrieve_infos( + file: &std::path::Path, +) -> Result { + let buffer = fs::read(file)?; + let elf = goblin::elf::Elf::parse(&buffer).unwrap(); + + let mut infos = LedgerAppInfos::default(); + + // All infos coming from the SDK are expected to be regrouped + // in various `.ledger.` (rust SDK <= 1.0.0) or + // `ledger. (rust SDK > 1.0.0) section of the binary. + for section in elf.section_headers.iter() { + if let Some(Ok(name)) = + elf.shdr_strtab.get(section.sh_name) + { + if name == "ledger.api_level" + { + // For rust SDK > 1.0.0, the API level is stored as a string (like C SDK) + infos.api_level = get_string_from_offset(&buffer, &(section.sh_offset as usize)); + } + else if name == ".ledger.api_level" + { + // For rust SDK <= 1.0.0, the API level is stored as a byte + infos.api_level = buffer[section.sh_offset as usize].to_string(); + } + else if name == "ledger.target_id" + { + infos.target_id = Some(get_string_from_offset(&buffer, &(section.sh_offset as usize))); + } + } + } + + let mut nvram_data = 0; + let mut envram_data = 0; + for s in elf.syms.iter() { + let symbol_name = elf.strtab.get(s.st_name); + let name = symbol_name.unwrap().unwrap(); + match name { + "_nvram_data" => nvram_data = s.st_value, + "_envram_data" => envram_data = s.st_value, + _ => (), + } + } + infos.size = envram_data - nvram_data; + Ok(infos) +} + +pub fn export_binary(elf_path: &std::path::Path, dest_bin: &std::path::Path) { + let objcopy = env::var_os("CARGO_TARGET_THUMBV6M_NONE_EABI_OBJCOPY") + .unwrap_or_else(|| "arm-none-eabi-objcopy".into()); + + Command::new(objcopy) + .arg(elf_path) + .arg(dest_bin) + .args(["-O", "ihex"]) + .output() + .expect("Objcopy failed"); + + let size = env::var_os("CARGO_TARGET_THUMBV6M_NONE_EABI_SIZE") + .unwrap_or_else(|| "arm-none-eabi-size".into()); + + // print some size info while we're here + let out = Command::new(size) + .arg(elf_path) + .output() + .expect("Size failed"); + + io::stdout().write_all(&out.stdout).unwrap(); + io::stderr().write_all(&out.stderr).unwrap(); +} + +pub fn install_with_ledgerctl( + dir: &std::path::Path, + app_json: &std::path::Path, +) { + let out = Command::new("ledgerctl") + .current_dir(dir) + .args(["install", "-f", app_json.to_str().unwrap()]) + .output() + .expect("fail"); + + io::stdout().write_all(&out.stdout).unwrap(); + io::stderr().write_all(&out.stderr).unwrap(); +} + +pub fn dump_with_ledgerctl( + dir: &std::path::Path, + app_json: &std::path::Path, + out_file_name: &str, +) { + let out = Command::new("ledgerctl") + .current_dir(dir) + .args([ + "install", + app_json.to_str().unwrap(), + "-o", + out_file_name, + "-f", + ]) + .output() + .expect("fail"); + + io::stdout().write_all(&out.stdout).unwrap(); + io::stderr().write_all(&out.stderr).unwrap(); +} diff --git a/cargo-ledger/tests/valid/Cargo.toml b/cargo-ledger/tests/valid/Cargo.toml new file mode 100644 index 00000000..d75f2633 --- /dev/null +++ b/cargo-ledger/tests/valid/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "test" +version = "0.0.0" + +[[bin]] +name = "test" +path = "" + +[package.metadata.ledger] +name = "TestApp" +curve = ["secp256k1"] +flags = "0x38" +path = ["'44/123"] + +[package.metadata.ledger.nanos] +icon = "./assets/nanos.gif" + +[package.metadata.ledger.nanox] +icon = "./assets/nanox.gif" + +[package.metadata.ledger.nanosplus] +icon = "./assets/nanosplus.gif" diff --git a/cargo-ledger/tests/valid_outdated/Cargo.toml b/cargo-ledger/tests/valid_outdated/Cargo.toml new file mode 100644 index 00000000..f661fbb9 --- /dev/null +++ b/cargo-ledger/tests/valid_outdated/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "test" +version = "0.0.0" + +[[bin]] +name = "test" +path = "" + +[package.metadata.nanos] +api_level = "1" +name = "TestApp" +curve = ["secp256k1"] +flags = "0" +icon = "nanos.gif" +icon_small = "nanox.gif" +path = ["'44/123"] diff --git a/cargo-ledger/tests/valid_variant/Cargo.toml b/cargo-ledger/tests/valid_variant/Cargo.toml new file mode 100644 index 00000000..1c0ff573 --- /dev/null +++ b/cargo-ledger/tests/valid_variant/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "test" +version = "0.0.0" + +[[bin]] +name = "test" +path = "" + +[package.metadata.ledger] +name = "TestApp" +curve = ["secp256k1"] +flags = "0x38" +path = ["'44/123"] +nanos.icon = "./assets/nanos.gif" +nanox.icon = "./assets/nanox.gif" +nanosplus.icon = "./assets/nanosplus.gif" From f8d66ea915da831969bb27e4fe6aa5fdf0ec4a66 Mon Sep 17 00:00:00 2001 From: GroM Date: Fri, 8 Dec 2023 11:03:08 +0100 Subject: [PATCH 2/6] [cargo-ledger] clippy + fmt OK --- cargo-ledger/src/main.rs | 15 +++++++++++---- cargo-ledger/src/utils.rs | 37 +++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/cargo-ledger/src/main.rs b/cargo-ledger/src/main.rs index 12d31982..d303a758 100644 --- a/cargo-ledger/src/main.rs +++ b/cargo-ledger/src/main.rs @@ -268,7 +268,7 @@ fn build_app( // Target ID according to target, in case it // is not present in the retrieved ELF infos. - let backup_targetid : String = match device { + let backup_targetid: String = match device { Device::Nanos => String::from("0x31100004"), Device::Nanox => String::from("0x33000004"), Device::Nanosplus => String::from("0x33100004"), @@ -315,11 +315,18 @@ fn build_app( .map(|path_str| path_str.to_string()) } }) - .map(|path_str| PathBuf::from(path_str)); + .map(PathBuf::from); let exe_filename = exe_path.file_name().unwrap().to_str(); let exe_parent = exe_path.parent().unwrap().to_path_buf(); - let apdu_file_path = output_dir.unwrap_or(exe_parent).join(exe_filename.unwrap()).with_extension("apdu"); - dump_with_ledgerctl(current_dir, &app_json, apdu_file_path.to_str().unwrap()); + let apdu_file_path = output_dir + .unwrap_or(exe_parent) + .join(exe_filename.unwrap()) + .with_extension("apdu"); + dump_with_ledgerctl( + current_dir, + &app_json, + apdu_file_path.to_str().unwrap(), + ); if is_load { install_with_ledgerctl(current_dir, &app_json); diff --git a/cargo-ledger/src/utils.rs b/cargo-ledger/src/utils.rs index ca279fdc..de0f8609 100644 --- a/cargo-ledger/src/utils.rs +++ b/cargo-ledger/src/utils.rs @@ -7,18 +7,19 @@ use std::process::Command; #[derive(Default, Debug)] pub struct LedgerAppInfos { pub api_level: String, - pub target_id : Option, + pub target_id: Option, pub size: u64, } -fn get_string_from_offset(vector: &Vec,offset: &usize) -> String { +fn get_string_from_offset(vector: &[u8], offset: &usize) -> String { // Find the end of the string (search for a line feed character) let end_index = vector[*offset..] .iter() - .position(|&x| x == '\n' as u8) + .position(|&x| x == b'\n') .map(|pos| *offset + pos) .unwrap_or(*offset); // Use the start offset if the delimiter position is not found - String::from_utf8(vector[*offset..end_index].to_vec()).expect("Invalid UTF-8") + String::from_utf8(vector[*offset..end_index].to_vec()) + .expect("Invalid UTF-8") } pub fn retrieve_infos( @@ -33,22 +34,22 @@ pub fn retrieve_infos( // in various `.ledger.` (rust SDK <= 1.0.0) or // `ledger. (rust SDK > 1.0.0) section of the binary. for section in elf.section_headers.iter() { - if let Some(Ok(name)) = - elf.shdr_strtab.get(section.sh_name) - { - if name == "ledger.api_level" - { + if let Some(Ok(name)) = elf.shdr_strtab.get(section.sh_name) { + if name == "ledger.api_level" { // For rust SDK > 1.0.0, the API level is stored as a string (like C SDK) - infos.api_level = get_string_from_offset(&buffer, &(section.sh_offset as usize)); - } - else if name == ".ledger.api_level" - { + infos.api_level = get_string_from_offset( + &buffer, + &(section.sh_offset as usize), + ); + } else if name == ".ledger.api_level" { // For rust SDK <= 1.0.0, the API level is stored as a byte - infos.api_level = buffer[section.sh_offset as usize].to_string(); - } - else if name == "ledger.target_id" - { - infos.target_id = Some(get_string_from_offset(&buffer, &(section.sh_offset as usize))); + infos.api_level = + buffer[section.sh_offset as usize].to_string(); + } else if name == "ledger.target_id" { + infos.target_id = Some(get_string_from_offset( + &buffer, + &(section.sh_offset as usize), + )); } } } From 282b93432ce3de957c18f932699ebc50886a0aa5 Mon Sep 17 00:00:00 2001 From: GroM Date: Fri, 8 Dec 2023 11:26:03 +0100 Subject: [PATCH 3/6] update ci worflow for cargo-ledger support --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 511d60a7..4fb8b1a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,19 @@ jobs: command: clippy args: --target ${{ matrix.target }} + clippy-cargo-ledger: + name: Run static analysis for cargo-ledger + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest + steps: + - name: Cargo clippy for cargo-ledger + uses: actions-rs/cargo@v1 + with: + command: clippy + args: -p cargo-ledger --no-deps + toolchain: "+1.72.0" + format: name: Check code formatting runs-on: ubuntu-latest @@ -62,6 +75,21 @@ jobs: command: build args: -p ledger_device_sdk --target ${{ matrix.target }} + build-cargo-ledger: + name: Build SDK + runs-on: ubuntu-latest + container: + image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest + steps: + - name: Clone + uses: actions/checkout@v4 + - name: Cargo build + uses: actions-rs/cargo@v1 + with: + command: build + args: -p cargo-ledger + toolchain: "+1.72.0" + test: name: Run unit and integration tests runs-on: ubuntu-latest From f9a13ae9649fa4c87b8cd8eb1b254de01009a38e Mon Sep 17 00:00:00 2001 From: GroM Date: Fri, 8 Dec 2023 13:05:16 +0100 Subject: [PATCH 4/6] run clippy nightly for sdk packages only --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fb8b1a5..d73bfbcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,7 @@ jobs: strategy: matrix: target: ["nanos", "nanox", "nanosplus"] + package: [include_gif, testmacro, ledger_secure_sdk_sys, ledger_device_sdk] steps: - name: Clone uses: actions/checkout@v4 @@ -29,7 +30,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --target ${{ matrix.target }} + args: -p ${{ matrix.package }} --target ${{ matrix.target }} clippy-cargo-ledger: name: Run static analysis for cargo-ledger @@ -37,6 +38,8 @@ jobs: container: image: ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest steps: + - name: Clone + uses: actions/checkout@v4 - name: Cargo clippy for cargo-ledger uses: actions-rs/cargo@v1 with: From 068f37f0d9df2a693acf4e213a4269d940a15199 Mon Sep 17 00:00:00 2001 From: GroM Date: Mon, 11 Dec 2023 10:52:53 +0100 Subject: [PATCH 5/6] [cargo-ledger] Bump version 1.2.1 --- cargo-ledger/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cargo-ledger/Cargo.toml b/cargo-ledger/Cargo.toml index 1769d875..50d212d6 100644 --- a/cargo-ledger/Cargo.toml +++ b/cargo-ledger/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-ledger" -version = "1.2.0" +version = "1.2.1" authors = ["yhql"] description = "Build and sideload Ledger Nano apps" categories = ["development-tools::cargo-plugins"] From df0bff1e0d9e8a73725cd1f838f0423c76722207 Mon Sep 17 00:00:00 2001 From: GroM Date: Mon, 11 Dec 2023 10:58:28 +0100 Subject: [PATCH 6/6] Revert "[cargo-ledger] Bump version 1.2.1" This reverts commit 068f37f0d9df2a693acf4e213a4269d940a15199. --- cargo-ledger/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cargo-ledger/Cargo.toml b/cargo-ledger/Cargo.toml index 50d212d6..1769d875 100644 --- a/cargo-ledger/Cargo.toml +++ b/cargo-ledger/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cargo-ledger" -version = "1.2.1" +version = "1.2.0" authors = ["yhql"] description = "Build and sideload Ledger Nano apps" categories = ["development-tools::cargo-plugins"]