Warning This documentation is a template and shall be updated with your own APDUs.
This documentation describes the APDU messages interface to communicate with the Boilerplate application.
The application covers the following functionalities :
- Get a public Boilerplate address given a BIP 32 path
- Sign a basic Boilerplate transaction given a BIP 32 path and raw transaction
- Sign a token transaction given a BIP 32 path, token address, and raw transaction
- Provide dynamic token metadata via CAL (Crypto Asset List) signed descriptors
- Retrieve the Boilerplate app version
- Retrieve the Boilerplate app name
The application interface can be accessed over HID or BLE
This command returns the public key for the given BIP 32 path.
The address can be optionally checked on the device before being returned.
| CLA | INS | P1 | P2 | Lc | Le |
|---|---|---|---|---|---|
| E0 | 05 | 00 : return address | 00 | variable | variable |
| 01 : display address and confirm before returning |
| Description | Length |
|---|---|
| Number of BIP 32 derivations to perform (max 10 levels) | 1 |
| First derivation index (big endian) | 4 |
| ... | 4 |
| Last derivation index (big endian) | 4 |
| Description | Length |
|---|---|
| Public Key length | 1 |
| Public Key | var |
| Chain code length | 1 |
| Chain code | var |
This command signs a Boilerplate transaction after having the user validate the transactions parameters.
The input data is the RLP encoded transaction streamed to the device in 255 bytes maximum data chunks.
| CLA | INS | P1 | P2 | Lc | Le |
|---|---|---|---|---|---|
| E0 | 06 | Chunk index | More chunks | variable | variable |
P1 - Chunk index:
0x00: First chunk (contains BIP32 path)0x01: Second chunk0x02: Third chunk- ...
0xFF: Maximum chunk index
P2 - More chunks flag:
0x00: Last chunk (no more data)0x80: More chunks to follow
When the entire transaction (including BIP32 path) fits in one APDU:
P1: 00 (first chunk)
P2: 00 (last chunk, no more data)When transaction data requires chunking across multiple APDUs:
First APDU (P1=0x00, P2=0x80): Second APDU (P1=0x01, P2=0x80): Third APDU (P1=0x02, P2=0x00):
| Description | Length |
|---|---|
| Number of BIP 32 derivations to perform (max 10 levels) | 1 |
| First derivation index (big endian) | 4 |
| ... | 4 |
| Last derivation index (big endian) | 4 |
| Description | Length |
|---|---|
| Transaction chunk | variable |
| Description | Length |
|---|---|
| Signature length | 1 |
| Signature | variable |
| v | 1 |
This command signs a mock token transaction after user on-screen validation.
The application will only display and sign transaction for tokens it has knowledge about (address/ticker/magnitude). Information is fetched from the internal hardcoded token database, or received from the PROVIDE_TOKEN_INFO API.
The tokens and the token transaction format are made up for showcase purposes.
| CLA | INS | P1 | P2 | Lc | Le |
|---|---|---|---|---|---|
| E0 | 07 | Chunk index | More chunks | variable | variable |
P1 - Chunk index:
0x00: First chunk (contains BIP32 path)0x01: Second chunk0x02: Third chunk- ...
0xFF: Maximum chunk index
P2 - More chunks flag:
0x00: Last chunk (no more data)0x80: More chunks to follow
When the entire transaction (including BIP32 path) fits in one APDU:
P1: 00 (first chunk)
P2: 00 (last chunk, no more data)When transaction data requires chunking across multiple APDUs:
First APDU (P1=0x00, P2=0x80): Second APDU (P1=0x01, P2=0x80): Third APDU (P1=0x02, P2=0x00):
| Description | Length |
|---|---|
| Number of BIP 32 derivations to perform (max 10 levels) | 1 |
| First derivation index (big endian) | 4 |
| ... | 4 |
| Last derivation index (big endian) | 4 |
| Description | Length |
|---|---|
| Token transaction chunk (includes 32-byte token address) | variable |
The token transaction has the following format:
- Nonce (8 bytes, big endian)
- To address (20 bytes)
- Token address (32 bytes) - must be in the token database
- Amount (8 bytes, big endian) - in token's smallest unit
- Memo length (varint)
- Memo (variable length)
| Description | Length |
|---|---|
| Signature length | 1 |
| Signature | variable |
| v | 1 |
This command returns boilerplate application version
| CLA | INS | P1 | P2 | Lc | Le |
|---|---|---|---|---|---|
| E0 | 03 | 00 | 00 | 00 | 04 |
None
| Description | Length |
|---|---|
| Application major version | 01 |
| Application minor version | 01 |
| Application patch version | 01 |
This command returns boilerplate application name
| CLA | INS | P1 | P2 | Lc | Le |
|---|---|---|---|---|---|
| E0 | 04 | 00 | 00 | 00 | 04 |
None
| Description | Length |
|---|---|
| Application name | variable |
This command provides dynamic token metadata to the device via a CAL (Crypto Asset List) signed descriptor. The descriptor is a TLV-encoded message signed by the CAL (a Ledger HSM) and the signature is validated using the device's PKI certificate infrastructure.
Once provided, the dynamic token information takes priority over the hardcoded token database for subsequent token transaction signing. The token metadata persists in RAM across commands (including regular transactions) until the app exits or a new token is provided.
This enables tokens to be added without firmware updates.
IMPORTANT FOR THIRD-PARTY DEVELOPERS:
- For this feature to work on a given application, the CAL needs to maintain the knowledge of the relevant tokens. This feature thus requires coordination with Ledger teams before implementation. Please reach out before implementing it.
- The hardcoded token database is a simpler token management method as it only involves the application.
Security model
- The CAL key must first be whitelisted with correct permissions on the device PKI, this is done by sending a certificate with the OS APDU header (starting by 0xB006)
- We can then send the TLV signed with the CAL key, the os_pki_verify lib call will ensure the TLV is signed by a whitelisted authority (the onboarded CAL key).
- In the test framework, we created a local fake CAL key and crafted a certificate with TEST permissions. It be accepted by Speculos but not by a real device.
Once a dynamic token is provided via PROVIDE_TOKEN_INFO, the token database lookup in get_token_info() follows this priority:
- Dynamic token (CAL): Check a dynamic token has been received
- Hardcoded database: Check built-in token database
- Unknown: Refuses to sign. A fallback method could be implemented instead (Display token address, blind sign, ...)
This means CAL-provided tokens override hardcoded database entries with the same address.
The dynamic token information is stored in RAM only (not in NVM) and persists across commands:
- Persists across
SIGN_TX(regular transactions) - Persists across
SIGN_TOKEN_TX(token transactions) - Persists across
GET_PUBLIC_KEYand other read-only commands - Cleared when app exits to dashboard
- Cleared after every transaction in SWAP context
- Overwritten when new
PROVIDE_TOKEN_INFOcommand received (only one slot is handled)
| CLA | INS | P1 | P2 | Lc | Le |
|---|---|---|---|---|---|
| E0 | 22 | 00 | 00 | variable | 00 |
The input data is a TLV-encoded descriptor with the following outer tags (all tags required, order matters for signature verification):
| Tag | Name | Length | Description |
|---|---|---|---|
| 0x01 | STRUCTURE_TYPE | 1 byte | Descriptor type (DYNAMIC_TOKEN = 0x90) |
| 0x02 | VERSION | 1 byte | Version of the serialization format |
| 0x03 | COIN_TYPE | 4 bytes | SLIP-44 coin type |
| 0x04 | APP_NAME | var | Name of the AppCoin. Case sensitive |
| 0x05 | TICKER | var | Token ticker that will be displayed on the device |
| 0x06 | MAGNITUDE | 1 byte | Decimals to format the amount in Ticker |
| 0x07 | TUID | var | Token Unique Identifier (app specific) |
| 0x08 | SIGNATURE | [70-72] | Signature of above fields |
TUID Field (tag 0x07): The TUID field content is application specific. It is recommended to make it a nested TLV structure itself. In the Boilerplate made up implementation it contains a single tag:
| Tag | Description | Length |
|---|---|---|
| 0x10 | Token address | 32 bytes |
Signature construction: The signature (tag 0x08) is computed over all tags EXCEPT tag 0x08 itself. In production context, the CAL is responsible for this signature. In our test framework, we use a mock CAL to dynamically craft signatures.
None (returns 0x9000 on success)
01 01 90 # STRUCTURE_TYPE = DYNAMIC_TOKEN (0x90)
02 01 01 # VERSION = 1
03 04 80 00 80 01 # CHAIN_ID = 0x80008001 (hardened 0x8001)
04 01 00 # SIGNER_ALGO = SECP256K1 (0x00)
05 01 08 # SIGNER_KEY = COIN_META (0x08)
06 40 <64 bytes signature> # DER_SIGNATURE (r || s)
07 22 # TUID length (34 bytes including sub-TLV)
10 20 <32 bytes> # Sub-tag 0x10: token address
08 07 # APP_DATA length (7 bytes)
04 55534454 # Ticker length (4) + "USDT"
06 # Decimals (6)| SW | Description |
|---|---|
| B009 | SW_INVALID_DYNAMIC_TOKEN - TLV parsing failed, signature verification failed, wrong coin type, or TUID validation failed |
| 6A86 | SWO_INCORRECT_P1_P2 - P1 or P2 not zero |
| 6A87 | SWO_WRONG_DATA_LENGTH - Invalid TLV structure length |
Note on testing: Speculos emulator accepts test PKI certificates for signature validation, but real Ledger devices reject them. This is a OS security feature independent of application code or build flags.
The following standard Status Words are returned for all APDUs.
| SW | SW name | Description |
|---|---|---|
| 6985 | SWO_CONDITIONS_NOT_SATISFIED | Rejected by user |
| 6A86 | SWO_INCORRECT_P1_P2 | Either P1 or P2 is incorrect |
| 6A87 | SWO_WRONG_DATA_LENGTH | Lc or minimum APDU length is incorrect |
| 6D00 | SWO_INVALID_INS | No command exists with INS |
| 6E00 | SWO_INVALID_CLA | Bad CLA used for this application |
| 6A80 | SWO_INCORRECT_DATA | Failed to parse raw transaction |
| 6985 | SWO_CONDITIONS_NOT_SATISFIED | Security issue with bad state |
| 6600 | SWO_SECURITY_ISSUE | Signature of raw transaction failed |
| B009 | SW_INVALID_DYNAMIC_TOKEN | Dynamic token TLV parsing/validation failed |
| 9000 | OK | Success |