Bringing Rust to Ledger hardware wallets
Beginning of this year I wanted to experiment with some embedded development. I ended up looking into Ledger cryptocurrency hardware wallet application development. I wouldn’t actually consider this true embedded development as a lot of the heavy lifting has been done and abstracted away by Ledger, but it’s still writing software for a very constrained environment. Ledger applications only get 4KB of RAM to utilise and most of the embedded development practices still apply (no heap allocations for instance).
I ended up writing an application for the Nano (previously known as RaiBlocks) cryptocurrency. In the process I got to experience various gotchas in the Ledger C SDK and also hitting the memory limits several times due to having to bundle blake2b and ed25519-donna libraries as that hashing function wasn’t included in the SDK.
Currently Ledger doesn’t really provide any tools to help developers with debugging their applications. So trying to find out why something is crashing is a process of making educated guesses, reinstalling the application and seeing if it crashes the USB dongle or not.
Several times during this process I got to thinking how Rust type-system and compiler could be a tremendous assets for developing such apps and how most of the issues could be caught at compile time.
After attending RustFest 2018 in Paris and seeing Jonathan Pallant present about his ventures into embedded Rust I decided to give it a go and see if I can get Rust code running on the Ledger dongle and if it could be used to improve the development experience. (You should also check out Jonathans talk about his Monotron project, it was one of my favourites from the conference.)
Does it even compile?
The first thing I needed to find out is if Rust can even target the Ledger secure element chip that the 3rd party apps are running on. After some Googling I found out that the Ledger uses a ST31 Cortex-M0 based chip and that Rust has support for the thumbv6m-none-eabi target.
I decided that the quickest & easiest way to find out if it actually works, is to compile the Rust code as a static library and then link it into a simple C based test app. That way the Ledger C SDK can be used to display data on the dongle screen and actually verify that the Rust code produced is running and not crashing the device.
The toughest part getting started with this was trying to find the relevant information as to how to configure the correct target for Cargo to use. After that, my experiment of writing some text data to a C buffer was up and running in no time. But there’s not much use to just processing raw data on Rust side. If Rust is to be used in an actual app it needs to be able to access at least some of the functionality provided by the Ledger firmware.
The next thing I wanted to experiment with was invoking some of the syscalls. I could’ve called the C functions from Rust, but that seemed too easy and in general I wanted to avoid jumping between C and Rust as much as possible. So I decided to try and invoke the supervisor (OS that implements the syscalls) directly from Rust, which required the use of unsafe Rust and some inline assembly. Took a bit of trial and error, but quite soon I was invoking the secure random data generation syscall that the firmware provides.
And now… Rust all the things!
So far I had spent slightly more than an evening on this experiment and I was on a roll.
One of the places where I knew Rust type-system could provide a lot of value was the event model that Ledger apps must conform to. The contract they have in place is that for every incoming message (event packet) there can be several data messages (command packets) and only one reply (status packet). In the C world it’s very easy to trip over it and accidentally send multiple replies to a single event and in doing so crashing the app. Using Rust move semantics an API could be designed that would prevent that kind of an error.
But to experiment with a Rust based event loop design the whole C SDK would have to be more-or-less by-passed. And in order to see if things work there needs to be a way to draw something on the screen. Since that is the only way of actually debugging things. Which meant that at the very least the UI layer needs to be re-implemented in Rust in order to test out the event loop API design ideas.
I set out to re-implement the low-level UI drawing syscalls and binary payload format before tackling the event loop. Doing it in this order meant that if something crashed it was due to faults in the UI protocol, not something else. And it would be easier to debug by hooking only a small portion of it into the working C SDK event handling logic. One evening of painstaking crashes and debugging later I had a very simple UI rendered from Rust.
Coming up and implementing the APIs for the UI framework and the new event loop took about a week of light tinkering and 2 evenings of chasing down bugs and breakages due to incorrect assumptions I had made along the way. But finally I got everything running and was able to completely remove the dependency on the official C SDK.
Now that I have proven to myself that my 3rd party Rust based SDK can work, I’m ready to publish the code on Github for others to see and give some feedback. Of course, there’s still tons of functionality missing and right now its not possible to build an app that would actually be useful in a cryptocurrency wallet context. But I do feel strongly that the Rust based SDK can provide enough benefits over the C SDK that this experiment is worth pursuing further.
Check out bolos-rs on Github and let me know what you think. I will also be publishing follow up posts that go into more details regarding the design decisions behind the APIs. Be sure to follow me to not miss those write-ups.