This is the official Ardor ledger wallet app for the Ledger Nano S and X devices
Ardor Wiki, Ledger Developers Portal
The ledger-app-builder
docker image is not currently published so you will need to fetch and build it manually. You only need to do this once and the image will be cached on your system.
Then you can switch to this repository and launch the ledger-app-builder
docker image to build the Ardor app. Just follow the standard instructions. In a nutshell:
$ docker run --rm -ti -v "$(realpath .):/app" ledger-app-builder:latest
root@656be163fe84:/app# make
Functional tests are written using the Ragger framework. The tests are located in the tests
folder.
pip install --extra-index-url https://test.pypi.org/simple/ -r requirements.txt
sudo apt-get update && sudo apt-get install qemu-user-static
To run all tests just issue the following command:
pytest --device all -v --tb=short tests/
Please note you need all the different versions compiled. You can compile them all by running the helper script ./make-all
inside the docker build image.
End to end tests are run from an Ardor node and the Speculos emulator. The Ardor node has some unit tests that use the Speculos emulator to test the Ledger app. Those tests also use the Speculos API to assert screen texts and send button press commands.
These tests require Docker (or a local Speculos installation) and Java 8 or newer.
To run the tests you need to build the app, load it into the Speculos emulator and run the tests from the Ardor node.
-
Build the Ledger app.
-
Run the app on the Speculos emulator using Docker. As an alternative you can use a locally installed Speculos emulator. In this case you will need to run the emulator on port 9999 and the API server on port 5000. The following command will run the emulator on Docker:
docker run --rm -it -v $(pwd):/speculos/apps -p 9999:9999 -p 5000:5000 ghcr.io/ledgerhq/speculos --display headless --seed "opinion change copy struggle town cigar input kit school patient execute bird bundle option canvas defense hover poverty skill donkey pottery infant sense orchard" --model nanos apps/bin/app.elf
-
Clone the Ardor node repository with the Ledger unit tests:
git clone https://[email protected]/sargue/ardor-ledger-test.git
-
Run tests:
./run-unit-tests.sh com.jelurida.ardor.integration.wallet.ledger.application.LedgerSpeculosSuite
To turn on logging on the Ledger app
- Install the debug firmware
- Enable debugging in the makefile (DEVEL = 1) - make sure not to commit this change
- Execute
make clean
and thenmake load
to generate the source code for all the PRINTF statements
In order to build the Nano S or Nano X version you just need to make sure the BOLOS_SDK
environment variable points to the corresponding SDK.
Make sure you rebuild the whole project when switching SDKs by executing make clean
and then make load
.
For example to build for the Nano X the compile command would be BOLOS_SDK=$NANOX_SDK make
Be careful not to underflow unsigned numeric types, for example:
n = (dataLength - 32) / sizeof(uint32_t);
This line would underflow in case the dataLength
variable is smaller than 32 which might lead to disaster
so please review carefully all substraction operations.
Ledger requires no compilation warnings anywhere in the code.
It's also required to pass the Clang static analyzer. The analyzer is included on the ledger-app-builder
docker image so, once there, you just issue the following commands:
make clean
scan-build --use-cc=clang -analyze-headers -enable-checker security -enable-checker unix -enable-checker valist -o scan-build --status-bugs make default
The project uses Github Actions to run the Clang static analyzer and the unit tests on each commit and pull request.
The CI is configured on the .github/workflows/ci-workflow.yml
with inspiration from the app-boilerplate
and the app-xrp
.
Do not include project header files inside other project header files to prevent complicating the dependencies.
Store constants and hardcoded values in config.h and config.c
The txnTypesList.c
source file is autogenerated by the createTxnTypes.py
script from the txtypes.txt
file. This step is automatically handled by the makefile.
txtypes.txt
should be generated externally by the Ardor developers whenever they add a new transaction type.
Changes to the txtypes.txt
should be picked up by the make process and a new txnTypesList.c
automatically generated. The txnTypesList.c
is not deleted on make clean
but you can use make realclean
that cleans everything.
The code flow starts at app_main
which uses a global try/catch to prevent the app from crashing on error.
The code loops on io_recv_command
waiting for the next command buffer, then calling the appropriate handler function implemented in the different .c files.
Commands are in the format of
0xE0 <command id byte> <p1 byte> <p2 byte> <sizeof buffer> <buffer>
Response buffers are usually in the form of
<return value byte> <buffer> <0x90> <0x00>
returnValues.h lists all the return statuses
To compile call
make
To compile and upload to the ledger device
make load
To get the amount of memory used in the app call the following command
readelf -s bin/app.elf | grep app_stack_canary
This will output the canary (which is at the end of the memory space) location then subtract 0x20001800
(Nano S) or
0xda7a0000
(Nano X) to get the actual used up space for the app.
The NanoS device has 4k of memory for the app and stack.
The app uses the SDK's built in app_stack_canary, it's activated in the makefile by the define HAVE_BOLOS_APP_STACK_CANARY
We advise to keep this flag always on, it just gives extra security and doesn't take up much CPU.
The way this works is it defines an int at the end of the stack, initializes it at startup and then check's against it every call to io_exchange, if it changes it throws an EXCEPTION_IO_RESET
, which should reset the app.
In order to debug a stack overflow, call check_canary() add different parts of the code to check if you have overflowed the stack.
Errors are propagated through the call stack and it's the command handler's or button handler's job to respond accordingly, clear the state if they manage it, and return the error back to the caller.
All return values for functions should be checked in every function.
Ardor signatures are based on a custom variant of the EC-KCDSA over Curve25519 algorithm which is not supported natively by Ledger.
To support standard BIP32 key derivation we implemented curve conversion for Ardor using the protocol Yaffe-Bender HD key derivation for EC-KCDSA, it's a derivation scheme that rides on top of the BIP32-Ed25519 HD key derivation scheme.
Technically a public key is a Point (X,Y) on a curve C. X,Y are integers modulo some field F with a base point on the curve G. The tuple (C, F, G) defines a "curve", in this paper we are dealing with the twisted edwards curve (ed25519) and curve25519.
We are using a morph function between ed25519 and curve25519 so that if Apoint = Knumber * BasePointED25519
on ed25519 then morph(Apoint) = Knumber * BasePointECKCDSA
on curve25119
Implementation for this function can be found in curveConversion.c
ed25519 public key is defined as PublicKeyED25519Point = CLAMP(SHA512(privateKey)[:32]) * ED25519BasePoint
Let's refer to CLAMP(SHA512(privateKey)[:32])
as KL
The derivation composition flow for path P is:
- os_derive_bip32_no_throw derives KLKR and chaincode for P using SLIP10 initialization on 512 bits master seed from bip39/bip32 24 words
- Derive PublicKeyED25519 using cx_eddsa_get_public_key and KL, the point is encoded as 65 bytes 0x04 XBigEndian YBigEndian
- PubleyKeyED25519YLE = convert(YBigEndian) - just reverse the bytes
- PublicKeyCurve25519X = morph(PublicKeyEED25519YLE)
Points on Curve25519 can be defined by the X coordinate (since each X coordinate has only one matching Y coordinate) so PublicKeyCurve25519X and KL should hold PublicKeyCurve25519X = KL * Curve25519BasePoint = Morphe(KL * ED25519BasePoint)
In EC-KCDSA publickey = privatekey^-1 * BasePoint, privateKey^-1 is referred to as the key seed, so KL is the key seed for the PublicKeyCurve25519X public key for path P.
Extra Notes:
-
ED25519 public keys are compressed into a Y point in little endian encoding having the MSB bit encode the parity of X (since each Y coordinate has two possible X values, X and -X in a prime field F which means if one is even the second is odd)
-
In order to derive public keys outside of the ledger (Master public key derivation), all we need is the ed25519 public key and chaincode, described in the derivation scheme.
-
Reference code for the derivation implementation can found in the Ardor source code
-
This repo implements SLIP10 master seed generation and BIP32 HD EdDSA key derivation in python for reference, this clone also implements master public key derivation for BIP32 EdDSA
-
Signing is using the formula
s * (x - h) mod order25519
. This differs from a standard implementation of EC-KCDSA (s * (x - h ⊕ r)
). In this custom variant,h
is calculated asH(m || r)
, wherer = [k]G and k = H(m || sk)
.