From 958951eff1ab79bae85dfdfd35b05cb63435630c Mon Sep 17 00:00:00 2001 From: Michal Date: Tue, 17 Dec 2024 21:37:53 +0100 Subject: [PATCH] fix(wallet-mobile): Fix HW TX signing --- .../src/features/Discover/common/ledger.ts | 13 ++ .../cardano/cip30/cip30-ledger.ts | 183 +++++++++++++++++- 2 files changed, 193 insertions(+), 3 deletions(-) diff --git a/apps/wallet-mobile/src/features/Discover/common/ledger.ts b/apps/wallet-mobile/src/features/Discover/common/ledger.ts index ebc008c17b..f0006c1dbd 100644 --- a/apps/wallet-mobile/src/features/Discover/common/ledger.ts +++ b/apps/wallet-mobile/src/features/Discover/common/ledger.ts @@ -3,6 +3,7 @@ import 'cbor-rn-prereqs' import { AddressType, AssetGroup, + BIP32Path, Certificate as LedgerCertificate, CertificateType, CredentialParamsType, @@ -220,6 +221,15 @@ export async function toLedgerSignRequest( additionalRequiredSigners: Array = [], ): Promise { const parsedCbor = await cbor.decode(rawTxBody) + const tagsState = await csl.hasTransactionSetTag( + await (await csl.FixedTransaction.newFromBodyBytes(await txBody.toBytes())).toBytes(), + ) + + if (tagsState === csl.TransactionSetsState.MixedSets) { + throw new Error('Transaction with mixed sets cannot be signed by Ledger') + } + + const txHasSetTags = tagsState === csl.TransactionSetsState.AllSetsHaveTag async function formatInputs(inputs: TransactionInputs): Promise> { const formatted = [] @@ -533,6 +543,9 @@ export async function toLedgerSignRequest( referenceInputs: formattedReferenceInputs, }, additionalWitnessPaths, + options: { + tagCborSets: txHasSetTags, + }, } } diff --git a/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30-ledger.ts b/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30-ledger.ts index 533e6a1d53..43aa96d01c 100644 --- a/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30-ledger.ts +++ b/apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30-ledger.ts @@ -1,6 +1,6 @@ -import {Transaction, WasmModuleProxy} from '@emurgo/cross-csl-core' +import {Bip32PublicKey, BootstrapWitness, Transaction, Vkeywitness, WasmModuleProxy} from '@emurgo/cross-csl-core' import {has_transaction_set_tag, TransactionSetsState} from '@emurgo/csl-mobile-bridge' -import {createSignedLedgerTxFromCbor} from '@emurgo/yoroi-lib' +import {Addressing, Bip44DerivationLevels, hashTransaction} from '@emurgo/yoroi-lib' import {normalizeToAddress} from '@emurgo/yoroi-lib/dist/internals/utils/addresses' import {HW, Wallet} from '@yoroi/types' @@ -11,6 +11,8 @@ import {assertHasAllSigners} from '../common/signatureUtils' import {signTxWithLedger} from '../hw/hw' import {CardanoTypes, YoroiWallet} from '../types' import {wrappedCsl} from '../wrappedCsl' +import {SignedTransactionData} from '@cardano-foundation/ledgerjs-hw-app-cardano' +import {YoroiUnsignedTx} from '../../types/yoroi' export const cip30LedgerExtensionMaker = (wallet: YoroiWallet, meta: Wallet.Meta) => { return new CIP30LedgerExtension(wallet, meta) @@ -49,9 +51,10 @@ class CIP30LedgerExtension { const bytes = await createSignedLedgerTxFromCbor( csl, cbor, - signedLedgerTx.witnesses, + signedLedgerTx, implementationConfig.derivations.base.harden.purpose, this.wallet.publicKeyHex, + getAddressedUtxos(this.wallet), ) return csl.Transaction.fromBytes(bytes) } finally { @@ -60,6 +63,180 @@ class CIP30LedgerExtension { } } +const createSignedLedgerTxFromCbor = async ( + wasm: WasmModuleProxy, + cbor: string, + signedData: SignedTransactionData, + purpose: number, + publicKeyHex: string, + senderUtxos: YoroiUnsignedTx['unsignedTx']['senderUtxos'], +): Promise => { + const key = await wasm.Bip32PublicKey.fromBytes(Buffer.from(publicKeyHex, 'hex')) + const fixedTx = await wasm.FixedTransaction.fromHex(cbor) + if (!fixedTx) throw new Error('invalid tx hex') + + const addressing = { + path: [ + purpose, + 2147485463, // CARDANO + 2147483648, + ], + startLevel: 1, + } + + const isSameArray = (array1: Array, array2: Array) => + array1.length === array2.length && array1.every((value, index) => value === array2[index]) + + const findWitness = (path: Array) => { + for (const witness of signedData.witnesses) { + if (isSameArray(witness.path, path)) { + return witness.witnessSignatureHex + } + } + + throw new Error(`buildSignedTransaction no witness for ${JSON.stringify(path)}`) + } + const keyLevel = addressing.startLevel + addressing.path.length - 1 + const witSet = await fixedTx.witnessSet() + const bootstrapWitnesses: Array = [] + const vkeys: Array = [] + + const seenVKeyWit = new Set() + const seenBootstrapWit = new Set() + + for (const utxo of senderUtxos) { + verifyFromBip44Root(utxo.addressing) + const witness = findWitness(utxo.addressing.path) + const addressKey = await derivePublicByAddressing(utxo.addressing, { + level: keyLevel, + key, + }) + + if (await wasm.ByronAddress.isValid(utxo.receiver)) { + const byronAddr = await wasm.ByronAddress.fromBase58(utxo.receiver) + const bootstrapWit = await wasm.BootstrapWitness.new( + await wasm.Vkey.new(await addressKey.toRawKey()), + await wasm.Ed25519Signature.fromBytes(Buffer.from(witness, 'hex')), + await addressKey.chaincode(), + await byronAddr.attributes(), + ) + const asString = Buffer.from(await bootstrapWit.toBytes()).toString('hex') + + if (seenBootstrapWit.has(asString)) { + continue + } + + seenBootstrapWit.add(asString) + bootstrapWitnesses.push(bootstrapWit) + continue + } + + const vkeyWit = await wasm.Vkeywitness.new( + await wasm.Vkey.new(await addressKey.toRawKey()), + await wasm.Ed25519Signature.fromBytes(Buffer.from(witness, 'hex')), + ) + const asString = Buffer.from(await vkeyWit.toBytes()).toString('hex') + + if (seenVKeyWit.has(asString)) { + continue + } + + seenVKeyWit.add(asString) + vkeys.push(vkeyWit) + } + + // add any staking key needed + for (const witness of signedData.witnesses) { + const addressing = { + path: witness.path, + startLevel: 1, + } + + if (witness.path[3] === 2) { + const stakingKey = await derivePublicByAddressing(addressing, { + level: keyLevel, + key, + }) + const vkeyWit = await wasm.Vkeywitness.new( + await wasm.Vkey.new(await stakingKey.toRawKey()), + await wasm.Ed25519Signature.fromBytes(Buffer.from(witness.witnessSignatureHex, 'hex')), + ) + const asString = Buffer.from(await vkeyWit.toBytes()).toString('hex') + + if (seenVKeyWit.has(asString)) { + continue + } + + seenVKeyWit.add(asString) + vkeys.push(vkeyWit) + } + } + + if (bootstrapWitnesses.length > 0) { + const bootstrapWitWasm = await wasm.BootstrapWitnesses.new() + + for (const bootstrapWit of bootstrapWitnesses) { + await bootstrapWitWasm.add(bootstrapWit) + } + + await witSet.setBootstraps(bootstrapWitWasm) + } + + const originalWitSet = await fixedTx.witnessSet() + const originalVkeys = await originalWitSet.vkeys() + const vkeyWitWasm = originalVkeys || (await wasm.Vkeywitnesses.new()) + + for (const vkey of vkeys) { + await vkeyWitWasm.add(vkey) + } + + await witSet.setVkeys(vkeyWitWasm) + + const signedTx = await wasm.Transaction.new(await fixedTx.body(), witSet, await fixedTx.auxiliaryData()) + + const id = await (await hashTransaction(wasm, await signedTx.toBytes())).toHex() + const ledgerTxHashHex = signedData.txHashHex + + if (id !== ledgerTxHashHex) { + console.log('signed tx', Buffer.from(await signedTx.toBytes()).toString('hex')) + console.log('original tx', cbor) + throw new Error(`buildLedgerSignedTx: TxId mismatch. Ledger: ${ledgerTxHashHex} Reconstructed: ${id}`) + } + + return signedTx.toBytes() +} + +export const verifyFromBip44Root = (addressing: Addressing): void => { + const accountPosition = addressing.startLevel + if (accountPosition !== Bip44DerivationLevels.PURPOSE.level) { + throw new Error(`verifyFromBip44Root addressing does not start from root`) + } + const lastLevelSpecified = addressing.startLevel + addressing.path.length - 1 + if (lastLevelSpecified !== Bip44DerivationLevels.ADDRESS.level) { + throw new Error(`verifyFromBip44Root incorrect addressing size`) + } +} + +const derivePublicByAddressing = async ( + addressing: Addressing, + startingFrom: { + key: Bip32PublicKey + level: number + }, +) => { + if (startingFrom.level + 1 < addressing.startLevel) { + throw new Error('derivePublicByAddressing: keyLevel < startLevel') + } + + let derivedKey = startingFrom.key + + for (let i = startingFrom.level - addressing.startLevel + 1; i < addressing.path.length; i++) { + derivedKey = await derivedKey.derive(addressing.path[i]) + } + + return derivedKey +} + const getHexAddressingMap = async (csl: WasmModuleProxy, wallet: YoroiWallet) => { const addressedUtxos = wallet.utxos.map(async (utxo: RawUtxo) => { const addressing = wallet.getAddressing(utxo.receiver)