Skip to content

fix(solana): disclose signed account identities on clear-sign screens - #591

Closed
BitHighlander wants to merge 3 commits into
alphafrom
fix/round5-solana-clearsign-identities
Closed

fix(solana): disclose signed account identities on clear-sign screens#591
BitHighlander wants to merge 3 commits into
alphafrom
fix/round5-solana-clearsign-identities

Conversation

@BitHighlander

Copy link
Copy Markdown
Owner

Problem

A systematic parser-to-prompt comparison found that several verified-tier
Solana clear-sign screens omitted signed account identities the parser
already extracted, letting a compromised host substitute the account being
funded, closed, delegated, reassigned, or authorized while presenting an
identical "verified" device prompt. Two additional, independent defects
compound this:

  1. Account-index bug (Stake/Vote Authorize): the current-authority
    signer was read from account index 1, which is the Clock sysvar in
    both canonical layouts ([0]=account, [1]=Clock sysvar, [2]=[SIGNER] authority), not the actual signer at index 2. Sibling instructions in
    the same file (Deactivate, Merge, Withdraw) already used the correct
    index.
  2. Missing bounds check: most branches classified an instruction
    SOL_TX_REVIEW_VERIFIED without checking the instruction actually
    listed enough accounts. A short account list makes copy_account()
    silently leave an identity zero-filled -- which base58-encodes to
    11111111111111111111111111111111, the real Solana System Program
    address, so a short-listed, spoofed transaction did not look obviously
    fake on screen.

Fixes #558, #559, #560, #588.

Fix

lib/firmware/solana.c

  • Stake Authorize and Vote Authorize: fix the account-index bug (1 → 2).
  • Add num_acct_indices >= N bounds checks to every verified-tier branch
    that was missing one (System nonce/assign/allocate, SPL Revoke/MintTo/
    Burn/CloseAccount/Freeze/Thaw/SyncNative, all of Stake and Vote, ATA
    Create). A short list now falls through to SOL_INSTR_UNKNOWN with
    has_unknown = true, which forces the whole transaction opaque
    (AdvancedMode) instead of clear-signing a fabricated identity --
    mirroring the one branch (TransferChecked) that already guarded this.

lib/firmware/fsm_msg_solana.h

  • Show the previously-hidden identity on every affected screen:
    • System: transfer (funding account), nonce advance/withdraw/init/
      authorize (nonce account), assign/allocate (affected account)
    • SPL: TransferChecked (source), Revoke/Burn/CloseAccount/Freeze/Thaw/
      SyncNative (affected account + mint where applicable)
    • Stake: Delegate/Withdraw/Authorize/Split/Deactivate/Merge (stake/vote
      accounts)
    • Vote: Authorize/Withdraw/UpdateValidator/UpdateCommission (vote
      account)
    • ATA Create: payer, ATA address, owner, mint
    • CloseAccount is the sharpest case: the rent-refund destination was
      never shown, so a host could redirect it while displaying only "Close
      token account?"
  • MintTo/Burn now scale by the signed decimals byte (pi->extra_u8) via
    the already-tested solana_formatTokenAmount(), instead of a raw
    integer -- matching TransferChecked, which already did this correctly.

unittests/firmware/solana.cpp: 9 new regression tests --

  • Both account-index fixes, proving the signer is read from index 2, not
    the sysvar at index 1.
  • The CloseAccount critical case: short-accounts → opaque, and canonical →
    both from/to correctly extracted.
  • Representative short-accounts-is-opaque coverage across System/SPL/
    Stake/ATA, including StakeDelegate (index 5, the deepest of any
    instruction here) to prove the bounds check isn't only reaching the
    shallow indices.

Verification

  • Built the emulator Docker image (scripts/emulator/Dockerfile) and ran
    the full firmware-unit suite locally: all 53 Solana tests pass
    (44 pre-existing + 9 new).
  • clang-format --dry-run --Werror clean on all three changed files.
  • cppcheck (same flags as CI's static-analysis job) clean on
    solana.c.
  • 3 pre-existing failures elsewhere in the full suite
    (Coins.TableSanity, Ethereum.TransformErc20RequiresCompleteCalldata ForClearSigning, Ethereum.TransformErc20RequiresBothTokensResolvable)
    are in files this change does not touch (confirmed via git diff --name-only) and are unrelated -- pre-existing on alpha's tip
    independent of this branch.
  • Every existing Solana test's account count was checked against the new
    minimums before pushing; none needed fewer accounts than the new
    bounds require, so no existing test needed adjustment.

Verified-tier Solana clear-sign screens omitted signed account identities
the parser already extracted, letting a compromised host substitute the
funding/destination/mint/authority account while showing an identical
device prompt. Also fixes two account-index bugs and a missing bounds
check that let short account lists silently zero-fill identities (which
base58-encodes to the real System Program address, so it did not look
obviously fake on screen).

lib/firmware/solana.c:
- Stake/Vote Authorize read the current-authority signer from account
  index 1, which is the Clock sysvar in both canonical layouts, not the
  signer at index 2. Sibling instructions in the same file (Deactivate,
  Merge, Withdraw) already used the correct index.
- Add num_acct_indices bounds checks to every verified-tier branch that
  was missing one (System nonce/assign/allocate, SPL Revoke/MintTo/Burn/
  CloseAccount/Freeze/Thaw/SyncNative, all of Stake and Vote, ATA Create),
  falling through to SOL_INSTR_UNKNOWN + has_unknown=true (forces the
  whole tx opaque) on a short list, mirroring the one branch
  (TransferChecked) that already guarded this.

lib/firmware/fsm_msg_solana.h:
- Show the previously-hidden identity on every affected instruction
  screen: System transfer/nonce-ops/assign/allocate (funding/nonce/
  affected account), SPL TransferChecked (source) and Revoke/Burn/
  CloseAccount/Freeze/Thaw/SyncNative (affected account + mint where
  applicable), Stake Delegate/Withdraw/Authorize/Split/Deactivate/Merge
  (stake/vote accounts), Vote Authorize/Withdraw/UpdateValidator/
  UpdateCommission (vote account), ATA Create (payer/address/owner/mint).
  CloseAccount is the sharpest case: the rent-refund destination was
  never shown, so a host could redirect it silently.
- MintTo/Burn now scale by the signed decimals byte (pi->extra_u8) via
  the already-tested solana_formatTokenAmount(), instead of rendering
  the raw integer -- matching TransferChecked, which already did this.

unittests/firmware/solana.cpp: 9 new regression tests -- both account-index
fixes (signer is index 2, not the sysvar at index 1), the CloseAccount
critical case (short-accounts opaque + canonical reveals both accounts),
and representative short-accounts-is-opaque coverage across System/SPL/
Stake/ATA proving the new bounds checks reach even the deepest index
(StakeDelegate, index 5).

Verified locally: built the emulator Docker image
(scripts/emulator/Dockerfile) and ran the full firmware-unit suite --
all 53 Solana tests (44 existing + 9 new) pass. clang-format and cppcheck
both clean. 3 pre-existing failures elsewhere in the suite (Coins.
TableSanity, Ethereum.TransformErc20*) are in files this change does not
touch and are unrelated (tracked separately, e.g. #473).

Fixes #558, #559, #560, #588.
Points at BitHighlander/python-keepkey#58 (merged 2bf87908), which
updates the Stake instruction integration tests to canonical Solana
account layouts matching the parser fixes in this PR.
@BitHighlander

Copy link
Copy Markdown
Owner Author

Do not merge current head d7a9da0: issue #595 identifies an exact remaining ATA bypass. The parser accepts num_acct_indices >= 3 but mint is index 3, so a three-account ATA Create remains VERIFIED, zero-fills/omits the mint, and defeats the claimed disclosure. The added test uses only two accounts and misses this off-by-one. Require >=4, set has_mint=true, and test 3 => OPAQUE / 4 => all four identities preserved. PR #587 already contains that exact guard and regression.

@BitHighlander

Copy link
Copy Markdown
Owner Author

PR #591 still does not fully remediate this issue at head d7a9da0. Both MintToChecked and BurnChecked enter the verified branch at data_len >= 9, but their signed decimals byte is at offset 9 and therefore requires 10 bytes. A 9-byte truncated Checked instruction is still VERIFIED and displayed with fabricated decimals=0. The same >= checks also admit trailing instruction bytes, while the parser ignores them.

The unchecked opcodes have no signed decimals at all, yet the shared display always calls solana_formatTokenAmount(..., extra_u8) with zero and labels the raw base-unit integer as tokens.

Required:

  • Checked forms: require canonical data_len == 10 and preserve a has_token_decimals state.
  • Unchecked forms: require canonical data_len == 9 and display base units, not fabricated token units.
  • Add regressions for checked length 9 => opaque, checked length 11 => opaque, unchecked length 9 => verified/base-units, and signed decimals 0/6/18.

PR #587 already implements and tests these exact boundaries. Do not close #560 from the current #591 head.

@BitHighlander

Copy link
Copy Markdown
Owner Author

Acknowledged — verified both findings directly against this branch's diff:

  1. ATA Create off-by-one (PR #591 ATA Create remains verified with a missing fourth mint account #595): this branch guards num_acct_indices >= 3 but reads mint from index 3, so a 3-account instruction stays VERIFIED with a zero-filled (spoofable) mint. Confirmed against PR Independent alpha audit remediation: signing, storage, entropy, and CI evidence #587's fix: it correctly requires >= 4 before classifying, with has_mint = true unconditional after that guard.
  2. MintToChecked/BurnChecked truncation (Solana MintToChecked and BurnChecked prompts ignore signed decimals #560 continuation): this branch accepts data_len >= 9 for both checked and unchecked forms, so a 9-byte Checked instruction (missing the signed decimals byte at offset 9) stays VERIFIED with a fabricated decimals=0. Confirmed PR Independent alpha audit remediation: signing, storage, entropy, and CI evidence #587 requires exact data_len == 10 for Checked / == 9 for unchecked, with a real has_token_decimals field instead of inferring it from length.

PR #587 is already 21 commits ahead of alpha / 0 behind (includes this fork's #589/#590/#593), and its Solana diff subsumes everything in this PR plus these two additional guards. Closing this PR without merging in favor of #587 rather than duplicating/reconciling two overlapping Solana diffs. Not closing #558/#559/#560/#588 from here — leaving that to #587's merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant