*These articles are my personal development logs, if I have wrong comprehension on some stuffs, feel free to let me know

**Get to know more about the blockchain technology we are developing: Zoobc in https://zoobc.com/

part 1: setting up development environment
part 2: Creating menu screens manually
part 4: Generating Public Key and signing data

In the previous article, I explained only a small implementation of UX for Ledger application, and it stands by itself, no communication with outside world yet. So in this article I would like to explain how outside entity communicates with Ledger so that we can do much more with it.

Before we move further, it is better if we understand a few important concepts:

C language

Because we code our applications in C, so we need to be aware of all the risk with C language as well. For example the common buffer overflow.

4 KiB Memory Limit

From documentation:

At the time of this writing, the default link script provided by the SDK for the Ledger Nano S allocates 4 KiB of RAM for applications to use. This 4 KiB has to be enough to store all global non-const and non-NVRAM variables as well as the call stack (which is currently set to 768 bytes by default, also defined in the link script).

Given the limited amount of RAM, we need to write our application carefully.

32-bit architecture

Ledger Nano S has BOLOS with 32-bit architecture. With this architecture, you have to understand that you need to deal with data more than 32 bits manually. You also need to pay attention on the memory alignment corresponds to this architecture.

Memory Alignment

In order to write a memory-efficient application in Ledger, we need to make sure our memory layout needs to be optimal. As in the memory limit point above, optimizing memory usage of our application is important so that our application can run properly across operations.

This concept will take a long explanation, so I suggest you go to the documentation.

Application Isolation

Ledger under the hood implements BOLOS (Blockchain Open Ledger Operating System). BOLOS has many secure features, one of them is application isolation .

From Ledger documentation:

Due to its limited amount of RAM, the Secure Element is designed to only support one application running at a time. This isolated model implies that once the application is running, no other application can spuriously disturb the SE-MCU link. It also means that BOLOS can give the currently running application full control of I/O with the device’s peripherals. This model allowed the BOLOS architecture to be designed in a way that gives applications as much control over the device’s features as possible. In essence, each application runs in a “virtual” device and can reconfigure all of the hardware as it pleases. BOLOS isolates the application from the other applications on the device, and restricts access to all areas of flash memory other than those exclusively allocated for the running application.

Understanding this concept will help us to ease about the possibility of memory leak where the data in other application is accessible from our application or vice versa. Also, it means that the application has to individually handle the message exchanges with outside entity.

Error Handling

For any instructions that may produce error, we need to handle the errors manually and carefully, otherwise our application may not work as expected when encountering errors during performing an operation.

To know the detail how errors should be handled, you can check the documentation.

Message format

Ledger communicates with outside entity with array of bytes. It’s actually up to us how we structure those bytes to satisfy our needs. But there’s a common implementation for the request message, it’s close to implementation of APDU.

The common request message format is as follows:

  1. Instruction class (1 Byte): it’s a single byte in the beginning of the message to make sure that we are not just processing all sort of data sent to our Ledger application
  2. Instruction Code (1 byte): It’s a byte to indicate what type of operation we want to perform with this message
  3. P1 & P2 / Instruction parameter 1 & 2 (1+1 bytes): this byte can be used add more context to the instruction that we want to perform. For example we have an instruction to sign a data, and when we set P1 as 0x0 we want to have the signature only returned and if P1 is set as 0x1 we also want the raw data to be returned
  4. Data (255 bytes): We usually put the data for our instruction to be performed in this part of the message. Also, we can also use the first 4 bytes to hold the length of the data sent to avoid buffer overflow while performing the instruction. We also need to note that, because of this limited size for our data, if we want to process data longer than the available size, we need to process the data in chunks.

Creating script to send message to Ledger

Now let’s start with some example. To understand better about the message format, I will start with the explanation of a sample script to send instruction with data to our ledger application. We will use python for this script.

from ledgerblue.comm import getDongle# getting sdk instance to communicate with Ledger
dongle = getDongle(True)
instructionClass = "80"
instructionCode = "01"
p1 = "00"
p2 = "00"
data = "68656c6c6f206d657373616765" # hello message
dataLength = int(len(data)/2).to_bytes(4,'little').hex()
# sending message to Ledger
publicKey = dongle.exchange(bytes.fromhex(instructionClass + instructionCode + p1 + p2 + dataLength + data))

Handler code for Instruction 0x1

Let’s see how is the APDU loop handles our instruction

static void sample_main(void)
{
volatile unsigned int rx = 0;
volatile unsigned int tx = 0;
volatile unsigned int flags = 0;
unsigned char messageLengthRaw[INT32_LENGTH];
int messageLength = 0;
char tempText[TEXT_SIZE];
// DESIGN NOTE: the bootloader ignores the way APDU are fetched. The only
// goal is to retrieve APDU.
// When APDU are to be fetched from multiple IOs, like NFC+USB+BLE, make
// sure the io_event is called with a
// switch event, before the apdu is replied to the bootloader. This avoid
// APDU injection faults.
for (;;)
{
volatile unsigned short sw = 0;
BEGIN_TRY
{
TRY
{
rx = tx;
tx = 0; // ensure no race in catch_other if io_exchange throws
// an error
rx = io_exchange(CHANNEL_APDU | flags, rx);
flags = 0;
// no apdu received, well, reset the session, and reset the
// bootloader configuration
if (rx == 0)
{
THROW(0x6982);
}
if (G_io_apdu_buffer[0] != 0x80)
{
THROW(0x6E00);
}
// unauthenticated instruction
switch (G_io_apdu_buffer[1])
{
case 0x00: // show message
flags |= IO_RESET_AFTER_REPLIED;
THROW(0x9000);
break;
case 0x01: // case 1
os_memmove(messageLengthRaw, G_io_apdu_buffer + 4, INT32_LENGTH);
messageLength = *(int *)messageLengthRaw;
os_memmove(tempText, G_io_apdu_buffer + 4 + 4, messageLength);
ui_show_messsage(tempText);
THROW(0x9000);
break;
case 0x02: // echo
tx = rx;
THROW(0x9000);
break;
case 0xFF: // return to dashboard
goto return_to_dashboard;
default:
THROW(0x6D00);
break;
}
}
CATCH_OTHER(e)
{
switch (e & 0xF000)
{
case 0x6000:
case 0x9000:
sw = e;
break;
default:
sw = 0x6800 | (e & 0x7FF);
break;
}
// Unexpected exception => report
G_io_apdu_buffer[tx] = sw >> 8;
G_io_apdu_buffer[tx + 1] = sw;
tx += 2;
}
FINALLY
{
}
}
END_TRY;
}
return_to_dashboard:
return;
}

Let’s not getting intimidated. Most of the code there just to make sure we cover un-handled instruction/message. The specific part that handles our instruction is this:

case 0x01: // case 1    // get the length of data
os_memmove(messageLengthRaw, G_io_apdu_buffer + 4, INT32_LENGTH);
messageLength = *(int *)messageLengthRaw;
// move the message to a temporary byte array
os_memmove(tempText, G_io_apdu_buffer + 4 + 4, messageLength);
ui_show_messsage(tempText);
THROW(0x9000);
break;

Put it simply, basically it only performs this:

  1. If the instruction code is 0x01 , get to this code block
  2. Get the length of data to avoid buffer overflow when copying the message data
  3. Show data by calling `ui_show_messsage(tempText)`
  4. After all of the operation finishes, reply the request message with success byte 0x9000

Function `ui_show_messsage` is basically copying the message into the array used for the displaying the message

static void ui_show_messsage(char text[TEXT_SIZE])
{
render_menu_text_top(text);
render_menu_text_bottom("");
UX_DISPLAY(bagl_ui_sample_nanos, NULL);
}

With these changes, once you send messages from the script above, you will see the message shown on the Ledger screen. Voila!! 🎉🎉

--

--