From 2f22503addce887c7002fb844724407d60ac14f0 Mon Sep 17 00:00:00 2001 From: Chris Duncan Date: Sat, 16 May 2026 00:35:57 -0700 Subject: [PATCH] Extract specific signing algorithms. --- src/lib/ledger/index.ts | 106 ++-------------------------------------- src/lib/ledger/sign.ts | 105 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 101 deletions(-) create mode 100644 src/lib/ledger/sign.ts diff --git a/src/lib/ledger/index.ts b/src/lib/ledger/index.ts index 0dd9950..12e8779 100644 --- a/src/lib/ledger/index.ts +++ b/src/lib/ledger/index.ts @@ -5,16 +5,16 @@ import { StatusCodes } from '@ledgerhq/errors' import { default as TransportBLE } from '@ledgerhq/hw-transport-web-ble' import { default as TransportHID } from '@ledgerhq/hw-transport-webhid' import { default as TransportUSB } from '@ledgerhq/hw-transport-webusb' -import { Account } from '../account' import { Block } from '../block' import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET } from '../constants' -import { bytes, dec, hex, utf8 } from '../convert' +import { bytes, dec, utf8 } from '../convert' import { Rpc } from '../rpc' import { Wallet } from '../wallet' import { _account } from './account' import { _cache } from './cache' import { _connect } from './connect' import { queue } from './queue' +import { signBlock, signNonce } from './sign' export type LedgerStatus = 'UNSUPPORTED' | 'DISCONNECTED' | 'BUSY' | 'LOCKED' | 'CONNECTED' export type LedgerTransport = typeof TransportHID | typeof TransportBLE | typeof TransportUSB @@ -28,7 +28,7 @@ export interface LedgerAccountResponse extends LedgerResponse { address: string | null } -interface LedgerSignResponse extends LedgerResponse { +export interface LedgerSignResponse extends LedgerResponse { signature: string | null, hash?: string } @@ -238,8 +238,8 @@ export class Ledger { } console.log('Waiting for signature confirmation on Ledger device...') const { status, signature, hash } = data instanceof Block - ? await this.#signBlock(index, data) - : await this.#signNonce(index, utf8.toBytes(data)) + ? await signBlock(this.#transport, index, data) + : await signNonce(this.#transport, index, utf8.toBytes(data)) if (status !== 'OK') { throw new Error('Signing with ledger failed', { cause: status }) } @@ -419,100 +419,4 @@ export class Ledger { console.log(event) } } - - /** - * Sign a block with the Ledger device. - * - * @param {number} index - Account number - * @param {object} block - Block data to sign - * @returns {Promise} Status, signature, and block hash - */ - static async #signBlock (index: number, block: Block): Promise { - if (block.signature !== undefined) { - throw new TypeError('Block signature already exists', { cause: block.signature }) - } - return queue(async () => { - try { - if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) { - throw new TypeError('Invalid account index') - } - if (!(block.link instanceof Uint8Array)) { - throw new TypeError('Invalid block link') - } - if (!(block.representative instanceof Account)) { - throw new TypeError('Invalid block representative') - } - - const account = dec.toBytes(index + HARDENED_OFFSET, 4) - const previous = block.previous - const link = block.link - const representative = hex.toBytes(block.representative.publicKey, 32) - const balance = hex.toBytes(BigInt(block.balance).toString(16), 16) - const data = new Uint8Array([...DERIVATION_PATH, ...account, ...previous, ...link, ...representative, ...balance]) - - const transport = await this.#transport.create(openTimeout, listenTimeout) - const response = await transport - .send(APDU_CODES.class, APDU_CODES.signBlock, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer) - .catch((err: any) => dec.toBytes(err.statusCode)) - .finally(async () => await transport.close()) as Uint8Array - - const statusCode = bytes.toDec(response.slice(-2)) as number - const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' - - if (response.byteLength === 2) { - return { status, signature: null } - } - if (response.byteLength === 98) { - const hash = bytes.toHex(response.slice(0, 32)) - const signature = bytes.toHex(response.slice(32, 96)) - return { status, signature, hash } - } else { - throw new Error('Unexpected byte length from device signature', { cause: response }) - } - } catch (err: any) { - console.error('Ledger.#signBlock()', err) - return { status: err.message, signature: null } - } - }) - } - - /** - * Sign a nonce with the Ledger device. - * - * @param {number} index - Account number - * @param {Uint8Array} nonce - 128-bit value to sign - * @returns {Promise} Status and signature - */ - static async #signNonce (index: number, nonce: Uint8Array): Promise { - return queue(async () => { - if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) { - throw new TypeError('Invalid account index') - } - if (nonce.byteLength !== 16) { - throw new RangeError('Nonce must be 16-byte string') - } - - const derivationAccount = dec.toBytes(index + HARDENED_OFFSET, 4) - const data = new Uint8Array([...DERIVATION_PATH, ...derivationAccount, ...nonce]) - - const transport = await this.#transport.create(openTimeout, listenTimeout) - const response = await transport - .send(APDU_CODES.class, APDU_CODES.signNonce, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer) - .catch((err: any) => dec.toBytes(err.statusCode)) - .finally(async () => await transport.close()) as Uint8Array - - const statusCode = bytes.toDec(response.slice(-2)) as number - const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' - - if (response.byteLength === 2) { - return { status, signature: null } - } - if (response.byteLength === 66) { - const signature = bytes.toHex(response.slice(0, 64)) - return { status, signature } - } else { - throw new Error('Unexpected byte length from device signature', { cause: response }) - } - }) - } } diff --git a/src/lib/ledger/sign.ts b/src/lib/ledger/sign.ts new file mode 100644 index 0000000..884e387 --- /dev/null +++ b/src/lib/ledger/sign.ts @@ -0,0 +1,105 @@ +//! SPDX-FileCopyrightText: 2025 Chris Duncan +//! SPDX-License-Identifier: GPL-3.0-or-later + +import { APDU_CODES, DERIVATION_PATH, LedgerSignResponse, LedgerTransport, STATUS_CODES, listenTimeout, openTimeout } from '.' +import { Account } from '../account' +import { Block } from '../block' +import { HARDENED_OFFSET } from '../constants' +import { bytes, dec, hex } from '../convert' +import { queue } from './queue' + +/** + * Sign a block with the Ledger device. + * + * @param {number} index - Account number + * @param {object} block - Block data to sign + * @returns {Promise} Status, signature, and block hash + */ +export async function signBlock (transport: LedgerTransport, index: number, block: Block): Promise { + if (block.signature !== undefined) { + throw new TypeError('Block signature already exists', { cause: block.signature }) + } + return queue(async () => { + try { + if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) { + throw new TypeError('Invalid account index') + } + if (!(block.link instanceof Uint8Array)) { + throw new TypeError('Invalid block link') + } + if (!(block.representative instanceof Account)) { + throw new TypeError('Invalid block representative') + } + + const account = dec.toBytes(index + HARDENED_OFFSET, 4) + const previous = block.previous + const link = block.link + const representative = hex.toBytes(block.representative.publicKey, 32) + const balance = hex.toBytes(BigInt(block.balance).toString(16), 16) + const data = new Uint8Array([...DERIVATION_PATH, ...account, ...previous, ...link, ...representative, ...balance]) + + const t = await transport.create(openTimeout, listenTimeout) + const response = await t + .send(APDU_CODES.class, APDU_CODES.signBlock, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer) + .catch((err: any) => dec.toBytes(err.statusCode)) + .finally(async () => await t.close()) as Uint8Array + + const statusCode = bytes.toDec(response.slice(-2)) as number + const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + + if (response.byteLength === 2) { + return { status, signature: null } + } + if (response.byteLength === 98) { + const hash = bytes.toHex(response.slice(0, 32)) + const signature = bytes.toHex(response.slice(32, 96)) + return { status, signature, hash } + } else { + throw new Error('Unexpected byte length from device signature', { cause: response }) + } + } catch (err: any) { + console.error('Ledger.#signBlock()', err) + return { status: err.message, signature: null } + } + }) +} + +/** + * Sign a nonce with the Ledger device. + * + * @param {number} index - Account number + * @param {Uint8Array} nonce - 128-bit value to sign + * @returns {Promise} Status and signature + */ +export async function signNonce (transport: LedgerTransport, index: number, nonce: Uint8Array): Promise { + return queue(async () => { + if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) { + throw new TypeError('Invalid account index') + } + if (nonce.byteLength !== 16) { + throw new RangeError('Nonce must be 16-byte string') + } + + const derivationAccount = dec.toBytes(index + HARDENED_OFFSET, 4) + const data = new Uint8Array([...DERIVATION_PATH, ...derivationAccount, ...nonce]) + + const t = await transport.create(openTimeout, listenTimeout) + const response = await t + .send(APDU_CODES.class, APDU_CODES.signNonce, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data as Buffer) + .catch((err: any) => dec.toBytes(err.statusCode)) + .finally(async () => await t.close()) as Uint8Array + + const statusCode = bytes.toDec(response.slice(-2)) as number + const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR' + + if (response.byteLength === 2) { + return { status, signature: null } + } + if (response.byteLength === 66) { + const signature = bytes.toHex(response.slice(0, 64)) + return { status, signature } + } else { + throw new Error('Unexpected byte length from device signature', { cause: response }) + } + }) +} -- 2.47.3