Ledger with Rust

Ledger with Rust

The Tari development community recently started talking about what it would take to add hardware wallet support for Tari.
There are a few challenges here:
Tari uses a different elliptic curve (Ristretto) to Bitcoin (secp256k1) and so there is probably no signing support “out of the box”.
We like Rust, but what does Rust support for hardware wallets look like?
Can we compile our crypto libraries to fit inside the extremely constrained resources of a crypto wallet (128kB in some cases)?
As a start, @SWvheerden tried to get some preliminary answers to these questions by trying not to brick a brand-new Ledger Nano S+. This is what he found.
So what’s the official stance?
The operating system behind all Ledger personal security devices is called the Blockchain Open Ledger Operating System, or
for short. Developer support is heavily skewed towards C code and Linux, but there are unofficial tools for developing in other languages, including Rust.
Although Windows gets unofficial support through its own
program which runs a very light Linux VM inside of windows, MacOS support is completely lacking.
For application development, support exists for mobile through
for android only, and for desktop, this is limited to
NodeJS
What are we going to try to accomplish?
We are going to see if we can create and load a sample BOLOS application with Rust without the use of an FFI onto a Ledger NanoS+ using MacOS. We also need to create a desktop application counterpart for this BOLOS application. Because we are going to attempt to use Rust this process should be multi-platform working on Linux and Windows.
The complete code for the BOLOS application and Desktop application can be found here:
Prerequisites
Before we can think about setting up our machine for the development of Ledger, we need to ensure that the machine is set up for the general development of Rust. This is going to focus on MacOS only, installing the dependencies for other OS should point to missing and otherwise required packages.
First is a Mac only requirement, installing
, you might get around this, but it will make your life much easier if you do.
. This needs to be installed if regardless of your choice of OS.
We can verify the installation with:
rustup 1.25.2 (17db695f1 2023-02-01)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.69.0-nightly (e1eaa2d5d 2023-02-06)`
Next up is
which importantly comes with pip3. This needs to be installed regardless of your choice of OS.
Verify this with
pip 22.3.1 from /opt/homebrew/lib/python3.11/site-packages/pip (python 3.11)
Lastly a Mac only requirement again,
, this can be installed from the app store. Make sure the version installed is higher than 14.2 verify this with:
Something to note, it’s possible to run multiple versions of Xcode on the same machine. This is required because to build a Tari Base node you need Xcode 14.0, see
The development environment
Now, let’s start with the Ledger specific dependencies now that we have a general dev machine setup.
For loading a BOLOS application to a Ledger device, Ledger has written a command, called
Cargo Ledger
. This we need to install with:
cargo install –git https://github.com/LedgerHQ/cargo-ledger
Next up we need to install the supporting Python libraries from Ledger to control Ledger devices,
LedgerCTL
pip3 install –upgrade protobuf setuptools ecdsa
pip3 install ledgerwallet
Lastly install the ARM GCC toolchain:
for your OS. We are using MacOS, so we can use brew with:
brew install armmbed/formulae/arm-none-eabi-gcc
That’s it for the development environment
The BOLOS application
The BOLOS application runs on the Ledger device itself and thus needs to be super simple. And to give you an idea of how low performant they are, they only have 64kB of RAM, use 32-bit only, and have about 1.5MB of storage with the NanoS+. Compare this to modern smartphones with an excess of 6GB of RAM and 128GB of storage.
This means we need to target a small footprint application targeting the llvm-target:
thumbv8m.main-none-eabi
support. Compiling Rust without
limits our choices of both internal and external libraries we can use, but it at least means we can run our Rust application on the Ledger.
Happily, Ledger does provide a
Rust SDK
that provides access to the secure chip running inside of BOLOS as well as communication out to some external applications.
For our demo BOLOS application, we will provide two commands:
We want
to return information about the BOLOS application.
will sign a challenge with a randomly created private key, returning the public key, nonce, and a Schnorr signature, all using the Ristretto curve.
Let’s go over the Rust code to sign the challenge and return the results:
match comm.next_event() {
io::Event::Command(Instruction::Sign) => {
// first bytes are instruction details
let challenge = ArrayString::<32>::from_bytes(comm.get(offset, offset + 32));
let k = RistrettoSecretKey::random();
let r = RistrettoSecretKey::random();
let signature = SchnorrSignature::sign_raw(&k, r, challenge.bytes()).unwrap();
let public_key = RistrettoPublicKey::from_secret_key(&k);
let sig = signature.get_signature().as_bytes();
let nonce = signature.get_public_nonce().as_bytes();
comm.append(&[1]); // version
comm.append(public_key.as_bytes());
BOLOS applications communicate using
. This means the first 5 bytes are information such as the instruction, arguments, etc. Because we know our challenge will be 32 bytes we can ignore byte 4 which will be the length of the byte and just straight up read our challenge from the buffer.
We then construct our signature using stripped-down code from
Tari crypto
We can then fill in the buffer with
to send the results back to the desktop application. Following the APDU standard we should send over the byte length of each field before its field, but because each field will be exactly 32 bytes we can ignore it.
Before compiling the code, we need to copy over the target information depending on which device we want to target. These target files are located with the
Rust SDK
We want to target the NanoS+ for now, so we copy that file over to our root directory of the project.
To compile our project we use the command:
cargo +nightly build -Zbuild-std –release –target=nanosplus.json
Out of interest for anyone following so far, this builds a 216kB application.
And finally to load it onto our ledger we use:
cargo +nightly ledger –load nanosplus
Following the prompts on the NanoS+ to install it.
The Desktop application
Luckily here we have the power to use Rust
so we don’t have the same limitations as the BOLOS counterpart. Also Zondax the developers of Polkadot have published some very nice helper libraries to help us develop our application:
Let’s highlight certain parts of the code.
The first important step is to get a connection to the ledger:
let ledger = TransportNativeHID::new(hidapi()).expect(“Could not get a device”);
The next important step is to construct the ADPU command:
let command = APDUCommand {
data: challenge.as_bytes().clone(),
It is important to note that the eventual command will insert as byte 5, the length of the data field before the data. But here with
we specify for the ledger we want to call the
command as above, and we include the challenge it must use.
Then we send it to the ledger with:
let result = ledger.exchange(&command2).unwrap();
We then read and verify our results by getting the data back from the ADPU:
let public_key = &result.data()[1..33];
let public_key = RistrettoPublicKey::from_bytes(public_key).unwrap();
let sig = &result.data()[33..65];
let sig = RistrettoSecretKey::from_bytes(sig).unwrap();
let nonce = &result.data()[65..97];
let nonce = RistrettoPublicKey::from_bytes(nonce).unwrap();
let signature = RistrettoSchnorr::new(nonce, sig);
let result = signature.verify(&public_key, &challenge);
println!(“sign: {}”, result);
Running them both
Because we don’t open up the ledger application with the desktop one, we need to open up the ledger application on the device first. After which we can run our desktop application. The application will generate a random challenge, which will then be sent to the ledger for signing.
The end result looks as follows:
Compiling legder_integration v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 3.41s
Running `target/debug/legder_integration`
package version: 0.0.1
This means we have a valid signature from the ledger for our random challenge.
Conclusion
Here we can see a very simple working example written in Rust and on a non-Linux machine. The complete code is available here:

Leave a Reply

Your email address will not be published. Required fields are marked *