Repository navigation
fix: bind Xtz dataToSign to verified txHex before signing - #10160
Conversation
Problem/goal: a tampered platform prebuild could supply a dataToSign authorizing an arbitrary multisig transfer (full wallet drain) while txHex still showed the user's intended recipient; signTransaction signed the platform-supplied bytes blindly and verifyTransaction never checked them. Ticket: CSHLD-2227. Implementation details: recompute the packed multisig payload locally with packGenericMultisigDataToSign (@taquito/michel-codec, already resolved transitively via @taquito/local-forging) and require a byte-for-byte match against the supplied dataToSign in both signTransaction and verifyTransaction; an absent dataToSign is unaffected. Sign fixtures were regenerated consistently and the sign tests now use the key derived from the already-published test seed (the [SECRET:...] placeholder has no resolver outside CI). Verification: full sdk-coin-xtz suite 137 passing / 0 failing; tamper tests fail when the check is stashed out; tsc build and eslint clean. Ticket: CSHLD-2227 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
|
|
@claude review for logical correctness, implementation completeness and regressions |
There was a problem hiding this comment.
I reviewed this PR and didn't find any bugs. It changes the XTZ multisig signing and verification path and adds a dependency, so a human from the module's code owners should still look at it.
What was reviewed:
- The new check in
signTransactionandverifyTransaction: it runs only whendataToSignis present, and it compares againstcontents[0], which is the operation the builder signsdataToSignfor. assertDataToSignMatchesTransfer: it rejects operations with no parameters or no counter, compares hex ignoring case, and repacks locally withpackDatawithout calling the node.- I didn't confirm that the local
packDataoutput is byte-identical to the node'spack_dataoutput. The PR description says it is, but checking that needs a test run.
Extended reasoning...
The change touches the XTZ coin module's transaction signing (signTransaction) and prebuild verification (verifyTransaction). It adds helpers in src/lib/utils.ts and declares @ taquito/michel-codec as a direct dependency. This is a security-sensitive surface: the check decides whether a user key signs a multisig spend authorization. CODEOWNERS assigns modules/sdk-coin-xtz/ to a specific team and package.json changes to SDK reviewers. The signing-path sensitivity and those ownership rules decided against auto-approval, even though the logic is small and has tests.
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
|
@claude review |
|
Claude finished @mukeshsp's task in 25s —— View job Review of #10160: bind Xtz
|
mukeshsp
left a comment
There was a problem hiding this comment.
approve with minor comments
The fix does what it describes. dataToSign is recomputed locally from the transfer decoded from txHex and compared byte for byte, in both signTransaction and verifyTransaction.
I traced the binding through the code:
- The recipient, amount and counter come from getMultisigTransferDataFromOperation. That is the same source verifyTransaction uses for outputs, so what the user verifies is what gets bound.
- The wallet contract address is bound as well.
- The signing override targets index 0 and addTransferSignature writes to contents[0]. The new check also reads contents[0], so the checked operation is the signed one.
- Skipping the check when dataToSign is absent keeps the fee-account and fully-signed paths working.
- yarn.lock already has the @taquito/michel-codec@^23.0.3 entry, so frozen-lockfile installs are fine.
Non-blocking comments
- Reveal as first operation (xtz.ts, assertDataToSignBoundToTxHex). If contents[0] is a reveal, the check throws "not a multisig transfer" even when a valid transfer sits at index 1. That fails closed, and the signing path already assumes the transfer is at index 0. Still, a reveal-first prebuild would now be rejected at verifyTransaction with a confusing error. Can you confirm the platform never emits reveal-first multisig prebuilds, or locate the first transaction op explicitly?
- Dead branch in the test (test/unit/util.ts, "should reject a non-multisig operation"). The fixture has no singlesig op, so the singlesigOperation === undefined branch is always taken and the second assert.throws never runs. Please drop the conditional and use the synthetic operation directly.
- Missing positive sign test. The new signTransaction tests only cover tampered dataToSign. A case with a matching dataToSign that signs successfully would guard against false rejects.
- Double decode. verifyTransaction now builds the transaction twice. It's cheap, but the first build's toJson() could be reused.
- Dependency range. The ^23.0.3 for michel-codec differs from the neighbouring ^23.0.2 entries. This is harmless, but you could align them.
Problem
A compromised or tampered platform prebuild could supply a
dataToSignauthorizing an arbitrary multisig transfer (e.g. full wallet drain to an attacker address) whiletxHexstill showed the user's intended recipient.signTransactionsigned the platform-supplied bytes blindly andverifyTransactionnever checked them — the two artifacts are independent inputs from the same untrusted prebuild.Goal
No user-key signature may ever be produced over a payload that is not derived from the transfer encoded in the verified
txHex.Fix
packGenericMultisigDataToSign(@taquito/michel-codec'spackData; the package was already resolved transitively via@taquito/local-forging, now declared directly) and require a byte-for-byte match against the supplieddataToSignin bothsignTransactionandverifyTransaction.dataToSignis unaffected (fee-account / fully-signed paths keep working).[SECRET:...]placeholder has no resolver outside CI).Testing
Full
sdk-coin-xtzsuite: 137 passing / 0 failing. The tamper tests fail when the check is stashed out (regression proven).tscbuild andeslintclean. LocalpackDataverified byte-identical to the node's/pack_dataoutput against the repo's own fixtures.Ticket: CSHLD-2227
🤖 Generated with Claude Code