//! SPDX-FileCopyrightText: 2025 Chris Duncan <chris@codecow.com>
//! SPDX-License-Identifier: GPL-3.0-or-later
-import { APDU_CODES, DERIVATION_PATH, LedgerSignResponse, LedgerTransport, LISTEN_TIMEOUT, OPEN_TIMEOUT, STATUS_CODES } from '.'
+import { APDU_CODES, DERIVATION_PATH, LedgerTransport, LISTEN_TIMEOUT, OPEN_TIMEOUT, STATUS_CODES } from '.'
import { Account } from '../account'
import { Block } from '../block'
import { HARDENED_OFFSET } from '../constants'
}
}
console.log('Waiting for signature confirmation on Ledger device...')
- const { status, signature, hash } = data instanceof Block
+ return data instanceof Block
? await signBlock(transport, index, data)
: await signNonce(transport, index, utf8.toBytes(data))
- if (status !== 'OK') {
- throw new Error('Signing with ledger failed', { cause: status })
- }
- if (data instanceof Block && hash !== data.hash) {
- throw new Error('Hash from Ledger does not match hash from block', { cause: `${hash} | ${data.hash}` })
- }
- if (signature == null) {
- throw new Error('Ledger silently failed to return signature')
- }
- return signature
} catch (err) {
console.error('Ledger.sign()', err)
throw new Error('Failed to sign block with Ledger', { cause: err })
* @param {object} block - Block data to sign
* @returns {Promise} Status, signature, and block hash
*/
-async function signBlock (transport: LedgerTransport, index: number, block: Block): Promise<LedgerSignResponse> {
+async function signBlock (transport: LedgerTransport, index: number, block: Block): Promise<string> {
if (block.signature !== undefined) {
throw new TypeError('Block signature already exists', { cause: block.signature })
}
- return queue<LedgerSignResponse>(async () => {
- try {
- 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(OPEN_TIMEOUT, LISTEN_TIMEOUT)
- 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
+ if (!(block.link instanceof Uint8Array)) {
+ throw new TypeError('Invalid block link')
+ }
+ if (!(block.representative instanceof Account)) {
+ throw new TypeError('Invalid block representative')
+ }
- const statusCode = bytes.toDec(response.slice(-2)) as number
- const status = STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'
+ 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])
- 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 }
- }
- })
+ const res = await queue(async () => req(transport, APDU_CODES.signBlock, data as Buffer))
+ if (res.hash !== block.hash) {
+ throw new Error('Hash from Ledger does not match hash from block', { cause: `${res.hash} | ${block.hash}` })
+ }
+ return res.signature
}
/**
* @param {Uint8Array} nonce - 128-bit value to sign
* @returns {Promise} Status and signature
*/
-async function signNonce (transport: LedgerTransport, index: number, nonce: Uint8Array<ArrayBuffer>): Promise<LedgerSignResponse> {
- return queue<LedgerSignResponse>(async () => {
- if (nonce.byteLength !== 16) {
- throw new RangeError('Nonce must be 16-byte string')
- }
+async function signNonce (transport: LedgerTransport, index: number, nonce: Uint8Array<ArrayBuffer>): Promise<string> {
+ 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 derivationAccount = dec.toBytes(index + HARDENED_OFFSET, 4)
+ const data = new Uint8Array([...DERIVATION_PATH, ...derivationAccount, ...nonce])
+ const res = await queue(async () => req(transport, APDU_CODES.signNonce, data as Buffer))
+ return res.signature
+}
- const t = await transport.create(OPEN_TIMEOUT, LISTEN_TIMEOUT)
- 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
+async function req (transport: LedgerTransport, command: typeof APDU_CODES.signBlock, data: Buffer): Promise<{ status: string, signature: string, hash: string }>
+async function req (transport: LedgerTransport, command: typeof APDU_CODES.signNonce, data: Buffer): Promise<{ status: string, signature: string }>
+async function req (transport: LedgerTransport, command: typeof APDU_CODES.signBlock | typeof APDU_CODES.signNonce, data: Buffer): Promise<{ status: string, signature: string, hash?: string }> {
+ const t = await transport.create(OPEN_TIMEOUT, LISTEN_TIMEOUT)
+ const response = await t
+ .send(APDU_CODES.class, command, APDU_CODES.paramUnused, APDU_CODES.paramUnused, data)
+ .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'
+ 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) {
+ if (status !== 'OK') {
+ throw new Error('Signing with ledger failed', { cause: status })
+ }
+
+ switch (response.byteLength) {
+ case (66): {
const signature = bytes.toHex(response.slice(0, 64))
return { status, signature }
- } else {
+ }
+ case (98): {
+ const hash = bytes.toHex(response.slice(0, 32))
+ const signature = bytes.toHex(response.slice(32, 96))
+ return { status, signature, hash }
+ }
+ case (2): {
+ throw new Error('Ledger silently failed to return signature', { cause: status })
+ }
+ default: {
throw new Error('Unexpected byte length from device signature', { cause: response })
}
- })
+ }
}