Skip to content

Commit

Permalink
fix(wallet-mobile): Fix HW TX signing
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeljscript committed Dec 17, 2024
1 parent c573d1e commit 958951e
Show file tree
Hide file tree
Showing 2 changed files with 193 additions and 3 deletions.
13 changes: 13 additions & 0 deletions apps/wallet-mobile/src/features/Discover/common/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'cbor-rn-prereqs'
import {
AddressType,
AssetGroup,
BIP32Path,

Check failure on line 6 in apps/wallet-mobile/src/features/Discover/common/ledger.ts

View workflow job for this annotation

GitHub Actions / check

'BIP32Path' is defined but never used. Allowed unused vars must match /^_/u
Certificate as LedgerCertificate,
CertificateType,
CredentialParamsType,
Expand Down Expand Up @@ -220,6 +221,15 @@ export async function toLedgerSignRequest(
additionalRequiredSigners: Array<string> = [],
): Promise<SignTransactionRequest> {
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<Array<TxInput>> {
const formatted = []
Expand Down Expand Up @@ -533,6 +543,9 @@ export async function toLedgerSignRequest(
referenceInputs: formattedReferenceInputs,
},
additionalWitnessPaths,
options: {
tagCborSets: txHasSetTags,
},
}
}

Expand Down
183 changes: 180 additions & 3 deletions apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30-ledger.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Transaction, WasmModuleProxy} from '@emurgo/cross-csl-core'
import {Bip32PublicKey, BootstrapWitness, Transaction, Vkeywitness, WasmModuleProxy} from '@emurgo/cross-csl-core'

Check failure on line 1 in apps/wallet-mobile/src/yoroi-wallets/cardano/cip30/cip30-ledger.ts

View workflow job for this annotation

GitHub Actions / check

Run autofix to sort these imports!
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'

Expand All @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -60,6 +63,180 @@ class CIP30LedgerExtension {
}
}

const createSignedLedgerTxFromCbor = async (
wasm: WasmModuleProxy,
cbor: string,
signedData: SignedTransactionData,
purpose: number,
publicKeyHex: string,
senderUtxos: YoroiUnsignedTx['unsignedTx']['senderUtxos'],
): Promise<Uint8Array> => {
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<number>, array2: Array<number>) =>
array1.length === array2.length && array1.every((value, index) => value === array2[index])

const findWitness = (path: Array<number>) => {
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<BootstrapWitness> = []
const vkeys: Array<Vkeywitness> = []

const seenVKeyWit = new Set<string>()
const seenBootstrapWit = new Set<string>()

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)
Expand Down

0 comments on commit 958951e

Please sign in to comment.