import { Account } from './account'
import { Block } from './block'
import { BIP44_COIN_NANO, BIP44_PURPOSE, HARDENED_OFFSET } from './constants'
-import { bytes, dec, hex } from './convert'
+import { bytes, dec, hex, utf8 } from './convert'
import { Rpc } from './rpc'
import { Wallet } from './wallet'
})
}
+ /**
+ * Sign a 16-byte nonce with the Ledger device. The actual messaage signed is a
+ * string which can be expressed as the following template literal:
+ *
+ * `Nano Signed Nonce:\n ${nonceBytes}`
+ *
+ * IMPORTANT: The current version of the Nano app for Ledger devices will NOT
+ * prompt users to confirm the signature. If valid, the nonce will immediately
+ * be signed and the signature returned without user interaction, similar to
+ * how receive blocks are automatically signed when auto-receive is configured.
+ * Plan for this eventuality if you implement this method to sign nonces.
+ *
+ * @param {number} index - Account number
+ * @param {string} nonce - 128-bit value to sign
+ */
+ static async sign (index: number, nonce: string): Promise<string>
/**
* Sign a block with the Ledger device.
*
* @param {Block} block - Block data to sign
* @param {Block} [frontier] - Previous block data to cache in the device
*/
- static async sign (index: number, block: Block, frontier?: Block): Promise<string> {
+ static async sign (index: number, block: Block, frontier?: Block): Promise<string>
+ static async sign (index: number, data: string | Block, frontier?: Block): Promise<string> {
try {
if (typeof index !== 'number') {
throw new TypeError('Index must be a number', { cause: index })
if (index < 0 || index >= HARDENED_OFFSET) {
throw new RangeError(`Index outside allowed range 0-${HARDENED_OFFSET}`, { cause: index })
}
+ if (typeof data !== 'string' && !(data instanceof Block)) {
+ throw new TypeError('Data to be signed must be a string nonce or a Block', { cause: data })
+ }
if (frontier != null) {
const { status } = await this.#cacheBlock(index, frontier)
if (status !== 'OK') {
}
}
console.log('Waiting for signature confirmation on Ledger device...')
- const { status, signature, hash } = await this.#signBlock(index, block)
+ const { status, signature, hash } = data instanceof Block
+ ? await this.#signBlock(index, data)
+ : await this.#signNonce(index, utf8.toBytes(data))
if (status !== 'OK') {
throw new Error('Signing with ledger failed', { cause: status })
}
- if (hash !== block.hash) {
- throw new Error('Hash from ledger does not match hash from block', { cause: `${hash} | ${block.hash}` })
+ 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')
* For compatibility with other Nano tools, the data is first hashed to a\r
* 32-byte value using BLAKE2b. The wallet must be unlocked prior to signing.\r
*\r
+ * Special note: This method can be used to sign a 16-byte nonce with a Ledger\r
+ * device. The actual messaage signed is a string which can be expressed as the\r
+ * following template literal:\r
+ *\r
+ * `Nano Signed Nonce:\n ${nonceBytes}`\r
+ *\r
+ * IMPORTANT: The current version of the Nano app for Ledger devices will NOT\r
+ * prompt users to confirm the signature. If valid, the nonce will immediately\r
+ * be signed and the signature returned without user interaction, similar to\r
+ * how receive blocks are automatically signed when auto-receive is configured.\r
+ * Plan for this eventuality if you implement this method to sign nonces.\r
+ *\r
* @param {number} index - Account to use for signing\r
* @param {(string|string[])} data - Arbitrary data to be hashed and signed\r
*/\r
async sign (index: number, data: string | string[]): Promise<string>\r
-\r
/**\r
* Signs a block using the private key of the account at the wallet index\r
* specified. The signature is appended to the signature field of the block\r
export async function _signData (wallet: Wallet, vault: Vault, index: number, data: string | string[]): Promise<string>
export async function _signData (wallet: Wallet, vault: Vault, index: unknown, data: unknown): Promise<string> {
try {
- if (wallet.type === 'Ledger') {
- throw new TypeError('Ledger wallet cannot sign arbitrary data')
- }
if (typeof index !== 'number') {
throw new TypeError('Index must be a number', { cause: index })
}
if (message.some(s => typeof s !== 'string')) {
throw new TypeError('Data to sign must be strings', { cause: data })
}
+ if (wallet.type === 'Ledger') {
+ return await Ledger.sign(index, message[0])
+ }
const hash = new Blake2b(32)
message.forEach(s => hash.update(utf8.toBytes(s)))
const { signature } = await vault.request<ArrayBuffer>({
assert.ok(/^[A-F0-9]{128}$/i.test(sendBlock.signature ?? ''))
})
+ await test('sign a 16-byte nonce', async () => {
+ const nonce = 'hello nano world'
+
+ const signature = await wallet.sign(0, nonce)
+
+ assert.exists(signature)
+ assert.ok(/^[A-F0-9]{128}$/i.test(signature))
+ })
+
+ await test('fail to sign invalid length nonces', async () => {
+ const nonceShort = 'hello world'
+ const nonceLong = 'hello world foobar'
+
+ await assert.rejects(wallet.sign(0, nonceShort))
+ await assert.rejects(wallet.sign(0, nonceLong))
+ })
+
await test('fail when using new', async () => {
assert.throws(() => new Wallet('Ledger'))
})