Ledger app requires open blocks to have zeroed frontier bytes instead of account public key, diverging from spec, so zero out "parent" block like Ledger expects if public key is the frontier.
Ensure block data is correct length before hashing.
Merge shared functionality of signing blocks and nonces, and document bug with Ledger nano app that is preventing nonces from being signed.
Skip online tests for now.
const data = [
PREAMBLE,
this.account.publicKey,
- this.previous,
+ this.previous.padStart(64, '0'),
this.representative.publicKey,
dec.toHex(this.balance, 32),
- this.link
+ this.link.padStart(64, '0')
]
const hash = new Blake2b(32)
data.forEach(str => hash.update(hex.toBytes(str)))
signBlock: 0x04,
signNonce: 0x05,
paramUnused: 0x00
-
})
export const UNITS: { [key: string]: number } = Object.freeze({
return wallet\r
}\r
\r
+ /**\r
+ * Removes encrypted secrets in storage and releases variable references to\r
+ * allow garbage collection.\r
+ */\r
+ async destroy (): Promise<void> {\r
+ await super.destroy()\r
+ await this.close()\r
+ }\r
+\r
/**\r
* Retrieves an existing Ledger wallet from session storage using its ID.\r
*\r
* Sign a nonce with the Ledger device.\r
*\r
* @param {number} index - Account number\r
- * @param {string} nonce - 128-bit string to sign\r
+ * @param {Uint8Array} nonce - 128-bit value to sign\r
* @returns {Promise} Status and signature\r
*/\r
- async sign (index: number, nonce: string): Promise<LedgerSignResponse>\r
- async sign (index: number = 0, input: string | SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerSignResponse> {\r
+ async sign (index: number, nonce: Uint8Array<ArrayBuffer>): Promise<LedgerSignResponse>\r
+ async sign (index: number = 0, input: Uint8Array<ArrayBuffer> | SendBlock | ReceiveBlock | ChangeBlock): Promise<LedgerSignResponse> {\r
if (typeof index !== 'number' || index < 0 || index >= HARDENED_OFFSET) {\r
throw new TypeError('Invalid account index')\r
}\r
const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)\r
const account = dec.toBytes(index + HARDENED_OFFSET, 4)\r
\r
- const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
- if (typeof input === 'string') {\r
+ let instruction: number\r
+ let data: Uint8Array\r
+\r
+ if (input instanceof Uint8Array) {\r
+ // nonce signing is currently broken: https://github.com/LedgerHQ/app-nano/pull/14\r
// input is a nonce\r
- const nonce = utf8.toBytes(input)\r
- if (nonce.length !== 16) {\r
+ instruction = LEDGER_ADPU_CODES.signNonce\r
+ if (input.byteLength !== 16) {\r
throw new RangeError('Nonce must be 16-byte string')\r
}\r
- const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...nonce])\r
- const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.signNonce, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer)\r
- .catch(err => dec.toBytes(err.statusCode)) as Uint8Array\r
- await transport.close()\r
-\r
- if (response.length === 2) {\r
- const statusCode = bytes.toDec(response) as number\r
- const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
- return { status, signature: null }\r
- }\r
-\r
- const signature = bytes.toHex(response.slice(0, 64))\r
- const statusCode = bytes.toDec(response.slice(-2)) as number\r
- const status = LEDGER_STATUS_CODES[statusCode]\r
- return { status, signature }\r
+ data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...input])\r
} else {\r
// input is a block\r
- const previous = hex.toBytes(input.previous)\r
- const link = hex.toBytes(input.link)\r
- const representative = hex.toBytes(input.representative.publicKey)\r
+ instruction = LEDGER_ADPU_CODES.signBlock\r
+ const previous = hex.toBytes(input.previous, 32)\r
+ const link = hex.toBytes(input.link, 32)\r
+ const representative = hex.toBytes(input.representative.publicKey, 32)\r
const balance = hex.toBytes(BigInt(input.balance).toString(16), 16)\r
- const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance])\r
- const response = await transport.send(LEDGER_ADPU_CODES.class, LEDGER_ADPU_CODES.signBlock, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer)\r
- .catch(err => dec.toBytes(err.statusCode)) as Uint8Array\r
- await transport.close()\r
-\r
- if (response.length === 2) {\r
- const statusCode = bytes.toDec(response) as number\r
- const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
- return { status, signature: null }\r
- }\r
+ data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance])\r
+ }\r
+ const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
+ const response = await transport.send(LEDGER_ADPU_CODES.class, instruction, LEDGER_ADPU_CODES.paramUnused, LEDGER_ADPU_CODES.paramUnused, data as Buffer)\r
+ .catch(err => dec.toBytes(err.statusCode)) as Uint8Array\r
+ await transport.close()\r
\r
- const hash = bytes.toHex(response.slice(0, 32))\r
- const signature = bytes.toHex(response.slice(32, 96))\r
- const statusCode = bytes.toDec(response.slice(-2)) as number\r
- const status = LEDGER_STATUS_CODES[statusCode]\r
- return { status, signature, hash }\r
+ const statusCode = bytes.toDec(response.slice(-2)) as number\r
+ const status = LEDGER_STATUS_CODES[statusCode] ?? 'UNKNOWN_ERROR'\r
+\r
+ if (status !== 'OK') {\r
+ return { status, signature: null }\r
}\r
+ if (response.byteLength > 66) {\r
+ const signature = bytes.toHex(response.slice(0, 64))\r
+ return { status, signature }\r
+ }\r
+\r
+ const hash = bytes.toHex(response.slice(0, 32))\r
+ const signature = bytes.toHex(response.slice(32, 96))\r
+ return { status, signature, hash }\r
+\r
}\r
\r
/**\r
}\r
input = res.contents\r
}\r
- return this.#cacheBlock(index, input)\r
+ const { status } = await this.#cacheBlock(index, input)\r
+ if (status !== 'OK') {\r
+ throw new Error(status)\r
+ }\r
+ return { status }\r
}\r
\r
/**\r
const purpose = dec.toBytes(BIP44_PURPOSE + HARDENED_OFFSET, 4)\r
const coin = dec.toBytes(BIP44_COIN_NANO + HARDENED_OFFSET, 4)\r
const account = dec.toBytes(index + HARDENED_OFFSET, 4)\r
- const previous = hex.toBytes(block.previous)\r
- const link = hex.toBytes(block.link)\r
- const representative = hex.toBytes(block.representative.publicKey)\r
- const balance = hex.toBytes(BigInt(block.balance).toString(16), 16)\r
- const signature = hex.toBytes(block.signature)\r
+ const previous = hex.toBytes(block.previous === block.account.publicKey ? '0' : block.previous, 32)\r
+ const link = hex.toBytes(block.link, 32)\r
+ const representative = hex.toBytes(block.representative.publicKey, 32)\r
+ const balance = hex.toBytes(block.balance.toString(16), 16)\r
+ const signature = hex.toBytes(block.signature, 64)\r
const data = new Uint8Array([LEDGER_ADPU_CODES.bip32DerivationLevel, ...purpose, ...coin, ...account, ...previous, ...link, ...representative, ...balance, ...signature])\r
\r
const transport = await this.DynamicTransport.create(this.openTimeout, this.listenTimeout)\r
})\r
\r
assert.equals(status, 'CONNECTED')\r
-\r
- status = (await wallet.close()).status\r
-\r
- assert.equals(status, 'OK')\r
+ assert.equals((await wallet.close()).status, 'OK')\r
})\r
})\r
import { NANO_TEST_VECTORS } from './VECTORS.js'\r
import { Bip44Wallet, Blake2bWallet, LedgerWallet } from '../dist/main.min.js'\r
\r
-await suite('Account derivation', async () => {\r
+await suite('BIP-44 account derivation', async () => {\r
await test('should derive the first account from the given BIP-44 seed', async () => {\r
const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
\r
await wallet.destroy()\r
})\r
+})\r
\r
+await suite('BLAKE2b account derivation', async () => {\r
await test('should derive accounts for a BLAKE2b wallet', async () => {\r
const wallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD)\r
await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)\r
/**\r
* This suite requires a connected unlocked Ledger device to execute tests.\r
*/\r
-await suite('should derive accounts for a Ledger device wallet', { skip: false || isNode }, async () => {\r
+await suite('Ledger device account derivation', { skip: false || isNode }, async () => {\r
const wallet = await LedgerWallet.create()\r
await wallet.connect()\r
\r
const rpc = new Rpc(process.env.NODE_URL ?? '', process.env.API_KEY_NAME)
-await suite('refreshing account info', { skip: false }, async () => {
+await suite('refreshing account info', { skip: true }, async () => {
await test('fetch balance, frontier, and representative', async () => {
const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)
})
})
-await suite('Fetch next unopened account', { skip: false }, async () => {
+await suite('Fetch next unopened account', { skip: true }, async () => {
await test('return correct account from test vector', async () => {
const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)
})
})
-await suite('Refreshing wallet accounts', { skip: false }, async () => {
+await suite('Refreshing wallet accounts', { skip: true }, async () => {
await test('should get balance, frontier, and representative for one account', async () => {
const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)
\r
import { assert, suite, test } from './GLOBALS.mjs'\r
import { NANO_TEST_VECTORS } from './VECTORS.js'\r
-import { SendBlock, ReceiveBlock, ChangeBlock } from '../dist/main.min.js'\r
+import { SendBlock, ReceiveBlock, ChangeBlock, LedgerWallet } from '../dist/main.min.js'\r
\r
await suite('Valid blocks', async () => {\r
\r
assert.equals(block.work, '')\r
})\r
})\r
+\r
+await suite('Ledger device signing tests', { skip: false }, async () => {\r
+ const wallet = await LedgerWallet.create()\r
+ const account = await new Promise(resolve => {\r
+ const button = document.createElement('button')\r
+ button.innerText = 'Unlock Ledger, then click to continue'\r
+ button.addEventListener('click', async (event) => {\r
+ await wallet.connect()\r
+ resolve(await wallet.account())\r
+ document.body.removeChild(button)\r
+ })\r
+ document.body.appendChild(button)\r
+ })\r
+\r
+ // nonce signing is currently broken: https://github.com/LedgerHQ/app-nano/pull/14\r
+ await test('should sign a nonce with a Ledger device', { skip: true }, async () => {\r
+ let status = await new Promise(resolve => {\r
+ const button = document.createElement('button')\r
+ button.innerText = 'Unlock Ledger, then click to continue'\r
+ button.addEventListener('click', async (event) => {\r
+ await wallet.connect()\r
+ const result = await wallet.sign(0, new TextEncoder().encode('0123456789abcdef'))\r
+\r
+ assert.equals(result.status, 'OK')\r
+ assert.equals(result.signature.toUpperCase(), '2BD2F905E74B5BEE3E2277CED1D1E3F7535E5286B6E22F7B08A814AA9E5C4E1FEA69B61D60B435ADC2CE756E6EE5F5BE7EC691FE87E024A0B22A3D980CA5B305')\r
+\r
+ document.body.removeChild(button)\r
+ resolve(result)\r
+ })\r
+ document.body.appendChild(button)\r
+ })\r
+ })\r
+\r
+ await test('should sign an open block and send block with a Ledger device', async () => {\r
+ const openBlock = new ReceiveBlock(\r
+ account,\r
+ '0',\r
+ NANO_TEST_VECTORS.RECEIVE_BLOCK.link,\r
+ NANO_TEST_VECTORS.RECEIVE_BLOCK.balance,\r
+ NANO_TEST_VECTORS.RECEIVE_BLOCK.representative,\r
+ '0'\r
+ )\r
+ await new Promise(resolve => {\r
+ const button = document.createElement('button')\r
+ button.innerText = 'Unlock Ledger, then click to test signing open ReceiveBlock'\r
+ button.addEventListener('click', async (event) => {\r
+ assert.nullish(openBlock.signature)\r
+ await openBlock.sign(0)\r
+ assert.ok(/[A-Fa-f0-9]{128}/.test(openBlock.signature))\r
+ resolve(document.body.removeChild(button))\r
+ })\r
+ document.body.appendChild(button)\r
+ })\r
+ assert.ok(/[A-Fa-f0-9]{128}/.test(openBlock.signature))\r
+ assert.ok(/[A-Fa-f0-9]{64}/.test(openBlock.hash))\r
+\r
+ const sendBlock = new SendBlock(\r
+ account,\r
+ NANO_TEST_VECTORS.RECEIVE_BLOCK.balance,\r
+ NANO_TEST_VECTORS.RECEIVE_BLOCK.account,\r
+ '0',\r
+ NANO_TEST_VECTORS.RECEIVE_BLOCK.representative,\r
+ openBlock.hash\r
+ )\r
+ await new Promise(resolve => {\r
+ const button = document.createElement('button')\r
+ button.innerText = 'Unlock Ledger, then click to test signing SendBlock'\r
+ button.addEventListener('click', async (event) => {\r
+ assert.nullish(sendBlock.signature)\r
+ await sendBlock.sign(0, openBlock)\r
+ assert.ok(/[A-Fa-f0-9]{128}/.test(sendBlock.signature))\r
+ resolve(document.body.removeChild(button))\r
+ })\r
+ document.body.appendChild(button)\r
+ })\r
+ })\r
+\r
+ await test('should fail sign a block with a Ledger device without caching frontier', async () => {\r
+ const block = new SendBlock(\r
+ NANO_TEST_VECTORS.SEND_BLOCK.account,\r
+ NANO_TEST_VECTORS.SEND_BLOCK.balance,\r
+ NANO_TEST_VECTORS.SEND_BLOCK.link,\r
+ '0',\r
+ NANO_TEST_VECTORS.SEND_BLOCK.representative,\r
+ NANO_TEST_VECTORS.SEND_BLOCK.previous,\r
+ NANO_TEST_VECTORS.SEND_BLOCK.work\r
+ )\r
+ assert.rejects(block.sign(0))\r
+ })\r
+\r
+ await wallet.destroy()\r
+})\r