CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-ledgerhq--hw-transport

Ledger Hardware Wallet common interface of the communication layer

Pending
Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

SecuritybySnyk

Pending

The risk profile of this skill

Overview
Eval results
Files

apdu-communication.mddocs/

APDU Communication

Low-level and high-level methods for sending Application Protocol Data Units (APDUs) to Ledger devices. APDUs are the standard command format for communicating with smart cards and hardware security modules.

Capabilities

Low-Level APDU Exchange

Direct APDU exchange method intended for transport implementations. Application developers should prefer the send() method instead.

/**
 * Low level API to communicate with the device
 * This method is for implementations to implement but should not be directly called
 * Instead, the recommended way is to use send() method
 * @param apdu The APDU data to send as a Buffer
 * @returns Promise of response data as Buffer
 */
exchange(apdu: Buffer): Promise<Buffer>;

Note: This method is abstract in the base Transport class and must be implemented by concrete transport implementations. Applications should use send() instead.

Atomic Exchange Implementation (Internal)

Internal method that ensures atomic execution of exchange operations with race condition protection and unresponsive device detection.

/**
 * Internal atomic exchange implementation with race condition protection
 * This method wraps exchange operations to ensure atomicity and provide unresponsive detection
 * @param f Function to execute atomically
 * @returns Promise resolving to the function result
 * @internal
 */
exchangeAtomicImpl(f: () => Promise<any>): Promise<any>;

Internal Properties:

class Transport<Descriptor> {
  /** Promise tracking current exchange operation for race condition prevention */
  exchangeBusyPromise: Promise<void> | null = null;
}

How it works:

  1. Checks if an exchange is already in progress (exchangeBusyPromise)
  2. Throws TransportRaceCondition if another operation is pending
  3. Creates a busy promise to track the operation
  4. Sets up unresponsive timeout detection
  5. Executes the provided function
  6. Emits "responsive" event if device was previously unresponsive
  7. Cleans up busy state and timeouts

This method is used internally by the send() method to ensure atomic operations and prevent race conditions.

High-Level APDU Sending

Wrapper on top of exchange() that provides a more convenient interface for sending APDU commands with automatic status code checking and error handling.

/**
 * Wrapper on top of exchange to simplify work of the implementation
 * @param cla Class byte (instruction class)
 * @param ins Instruction byte (command code)
 * @param p1 Parameter 1 byte
 * @param p2 Parameter 2 byte  
 * @param data Data payload (optional, default: empty buffer)
 * @param statusList List of accepted status codes (default: [0x9000])
 * @returns Promise of response buffer (without status bytes)
 */
send(
  cla: number,
  ins: number, 
  p1: number,
  p2: number,
  data?: Buffer = Buffer.alloc(0),
  statusList?: Array<number> = [StatusCodes.OK]
): Promise<Buffer>;

Usage Examples:

import Transport, { StatusCodes } from "@ledgerhq/hw-transport";

// Assuming you have a transport instance
const transport = await MyTransport.create();

// Get application version (example APDU)
const versionResponse = await transport.send(
  0xB0,  // CLA: application class
  0x01,  // INS: get version instruction
  0x00,  // P1: parameter 1
  0x00   // P2: parameter 2
  // data: omitted (empty)
  // statusList: omitted (defaults to [0x9000])
);

// Send data with custom status codes
const data = Buffer.from("hello", "utf8");
const response = await transport.send(
  0xB0,                           // CLA
  0x02,                           // INS  
  0x00,                           // P1
  0x00,                           // P2
  data,                           // Data payload
  [StatusCodes.OK, 0x6A80]        // Accept OK or INCORRECT_DATA
);

// Handle large data (automatically validates < 256 bytes)
try {
  const largeData = Buffer.alloc(300); // This will throw an error
  await transport.send(0xB0, 0x03, 0x00, 0x00, largeData);
} catch (error) {
  if (error.id === "DataLengthTooBig") {
    console.error("Data too large:", error.message);
  }
}

APDU Command Structure

APDUs follow the ISO 7816-4 standard structure:

| CLA | INS | P1 | P2 | Lc | Data | Le |

Where:

  • CLA (Class): Instruction class (1 byte)
  • INS (Instruction): Command code (1 byte)
  • P1, P2 (Parameters): Command parameters (1 byte each)
  • Lc (Length): Data length (1 byte, automatically set by send())
  • Data: Command data (0-255 bytes)
  • Le (Expected): Expected response length (omitted in this implementation)

Response Structure

APDU responses contain:

| Data | SW1 | SW2 |

Where:

  • Data: Response data (variable length)
  • SW1, SW2 (Status Words): 2-byte status code

The send() method automatically:

  1. Constructs the proper APDU format
  2. Sends via exchange()
  3. Extracts the status code (SW1|SW2)
  4. Checks against statusList
  5. Returns only the data portion (without status bytes)
  6. Throws TransportStatusError for unaccepted status codes

Common APDU Patterns

Get Device Information

// Get app version
const version = await transport.send(0xB0, 0x01, 0x00, 0x00);

// Get device public key
const publicKey = await transport.send(
  0xE0,           // CLA for crypto operations
  0x02,           // INS for get public key
  0x00,           // P1: display on screen
  0x00,           // P2: return key format
  derivationPath  // BIP32 path as Buffer
);

Send Transaction Data

// Sign transaction in chunks
const chunks = splitTransactionIntoChunks(transactionData);

for (let i = 0; i < chunks.length; i++) {
  const isFirst = i === 0;
  const isLast = i === chunks.length - 1;
  
  const response = await transport.send(
    0xE0,                    // CLA
    0x04,                    // INS for transaction signing
    isFirst ? 0x00 : 0x80,   // P1: first chunk vs continuation
    isLast ? 0x80 : 0x00,    // P2: last chunk marker
    chunks[i]                // Chunk data
  );
  
  if (isLast) {
    // Final response contains signature
    return response;
  }
}

Handle User Confirmation

import { StatusCodes } from "@ledgerhq/hw-transport";

try {
  const signature = await transport.send(
    0xE0, 0x04, 0x01, 0x00,
    transactionData,
    [StatusCodes.OK, StatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED]
  );
  
  console.log("Transaction signed:", signature);
  
} catch (error) {
  if (error instanceof TransportStatusError) {
    if (error.statusCode === StatusCodes.CONDITIONS_OF_USE_NOT_SATISFIED) {
      console.log("User rejected the transaction");
    } else {
      console.error("Device error:", error.statusText);
    }
  }
}

Error Handling

APDU communication can fail with various errors:

Data Length Validation

// Automatic validation prevents oversized commands
try {
  const largeData = Buffer.alloc(256); // Exactly 256 bytes
  await transport.send(0xB0, 0x01, 0x00, 0x00, largeData);
} catch (error) {
  // TransportError with id "DataLengthTooBig"
  console.error(error.message); // "data.length exceed 256 bytes limit. Got: 256"
}

Status Code Errors

import { StatusCodes, TransportStatusError } from "@ledgerhq/hw-transport";

try {
  await transport.send(0xB0, 0xFF, 0x00, 0x00); // Invalid instruction
} catch (error) {
  if (error instanceof TransportStatusError) {
    console.log("Status code:", error.statusCode.toString(16)); // "6d00"
    console.log("Status text:", error.statusText); // "INS_NOT_SUPPORTED"
    console.log("Message:", error.message); // "Ledger device: Instruction not supported (0x6d00)"
  }
}

Common Status Codes

  • 0x9000 (OK): Success
  • 0x6700 (INCORRECT_LENGTH): Wrong data length
  • 0x6982 (SECURITY_STATUS_NOT_SATISFIED): Device locked or access denied
  • 0x6985 (CONDITIONS_OF_USE_NOT_SATISFIED): User denied the operation
  • 0x6A80 (INCORRECT_DATA): Invalid data format
  • 0x6B00 (INCORRECT_P1_P2): Invalid parameters
  • 0x6D00 (INS_NOT_SUPPORTED): Instruction not supported
  • 0x6E00 (CLA_NOT_SUPPORTED): Class not supported

docs

apdu-communication.md

configuration.md

device-management.md

error-handling.md

events-lifecycle.md

index.md

tile.json