fix: mark legacy utxo transaction as deprecated and disable btc legacy transactions
What changed, and why it matters
This commit disables the older, raw-protobuf signing path for Bitcoin and Dogecoin transactions in the Keystone 3 hardware wallet firmware. It keeps the legacy path active only for Bitcoin Cash (BCH), Dash (DASH), and Litecoin (LTC). The change is described as a deprecation and hardening measure, not as a fix for a specific reported vulnerability. The patch adds rejection checks at multiple entry points (parsing, checking, signing, and QR-code type detection) so that unsupported legacy UTXO transactions fail with a clear error message instead of being processed.
Treat this as a hardening/deprecation change rather than a confirmed vulnerability fix. If you operate a Keystone 3 device, ensure firmware is updated so that Bitcoin transactions are forced through the PSBT path. Review downstream companion apps to confirm they no longer send raw-protobuf BTC or DOGE SignTransaction payloads, since those will now fail. If you previously relied on raw-protobuf BTC signing, migrate to PSBT. No immediate emergency response is indicated by the diff alone, but the change suggests the legacy path was considered less safe.
Security signals we found
Disables a legacy transaction parsing/signing code path for Bitcoin and Dogecoin
Adds explicit rejection checks before raw transaction parsing, checking, signing, and view routing
Switches discriminator from coin_code string to protobuf transaction variant to prevent spoofing
Marks raw-protobuf Bitcoin transactions as deprecated in favor of PSBT
Adds unit tests verifying rejection of spoofed coin codes for unsupported legacy variants
Evidence from the diff
The patch marks the raw-protobuf UTXO transaction module as deprecated and introduces two helper predicates in app_bitcoin::network: is_legacy_utxo_transaction (matches BTC, BCH, DASH, LTC, DOGE raw protobuf variants) and is_supported_legacy_utxo_transaction (matches only BCH, DASH, LTC). It then enforces this policy in four places: utxo_parse_keystone, get_signed_tx, build_check_result, and get_view_type_from_keystone. Bitcoin and Dogecoin raw-protobuf transactions are now rejected before signing/checking/view routing. The coin_code string is deliberately no longer used as the discriminator; the protobuf transaction variant is. Unit tests confirm BTC and DOGE are rejected even when coin_code is spoofed, and BCH/DASH/LTC are accepted regardless of coin_code.
Changed components
rust/apps/bitcoin/src/lib.rsrust/apps/bitcoin/src/network.rsrust/apps/bitcoin/src/transactions/mod.rsrust/rust_c/src/bitcoin/legacy.rsrust/rust_c/src/common/keystone.rsrust/rust_c/src/common/ur_ext.rsInspect captured patch +134 / −16
### rust/apps/bitcoin/src/lib.rs
@@ -21,13 +21,15 @@ use bitcoin::secp256k1::ecdsa::{RecoverableSignature, RecoveryId};
use bitcoin::secp256k1::Message;
use bitcoin::sign_message;
use either::{Left, Right};
+#[allow(deprecated)]
pub use transactions::legacy::sign_legacy_tx;
pub use transactions::parsed_tx;
pub use transactions::psbt::parsed_psbt;
use ur_registry::pb::protoc;
use crate::errors::{BitcoinError, Result};
use crate::parsed_tx::{ParseContext, ParsedTx, TxParser};
+#[allow(deprecated)]
use crate::transactions::legacy::TxData;
use crate::transactions::psbt::wrapped_psbt::WrappedPsbt;
use crate::transactions::tx_checker::TxChecker;
### rust/apps/bitcoin/src/network.rs
@@ -1,6 +1,8 @@
use crate::errors::BitcoinError;
use alloc::string::{String, ToString};
use core::str::FromStr;
+use ur_registry::pb::protoc::sign_transaction::Transaction;
+use ur_registry::pb::protoc::SignTransaction;
pub trait NetworkT {
fn get_unit(&self) -> String;
@@ -17,6 +19,31 @@ pub struct LargeFeePolicy {
pub rate_threshold_per_vbyte: Option<u64>,
}
+pub const UNSUPPORTED_LEGACY_UTXO_MESSAGE: &str =
+ "unsupported legacy UTXO transaction; only BCH, DASH and LTC are allowed";
+
+pub fn is_legacy_utxo_transaction(sign_tx: &SignTransaction) -> bool {
+ matches!(
+ sign_tx.transaction.as_ref(),
+ Some(
+ Transaction::BtcTx(_)
+ | Transaction::BchTx(_)
+ | Transaction::DashTx(_)
+ | Transaction::LtcTx(_)
+ | Transaction::DogeTx(_)
+ )
+ )
+}
+
+/// The deprecated raw-protobuf path is retained only for these transaction
+/// variants. `coin_code` is deliberately not used as a discriminator.
+pub fn is_supported_legacy_utxo_transaction(sign_tx: &SignTransaction) -> bool {
+ matches!(
+ sign_tx.transaction.as_ref(),
+ Some(Transaction::BchTx(_) | Transaction::DashTx(_) | Transaction::LtcTx(_))
+ )
+}
+
#[derive(Debug, Clone)]
pub enum Network {
Bitcoin,
@@ -158,3 +185,39 @@ impl NetworkT for CustomNewNetwork {
}
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::{is_legacy_utxo_transaction, is_supported_legacy_utxo_transaction};
+ use ur_registry::pb::protoc::sign_transaction::Transaction;
+ use ur_registry::pb::protoc::{BchTx, BtcTx, DashTx, DogeTx, LtcTx, SignTransaction};
+
+ fn sign_tx(coin_code: &str, transaction: Transaction) -> SignTransaction {
+ SignTransaction {
+ coin_code: coin_code.to_string(),
+ transaction: Some(transaction),
+ ..Default::default()
+ }
+ }
+
+ #[test]
+ fn bch_dash_and_ltc_variants_are_supported_without_using_coin_code() {
+ let bch = sign_tx("ignored", Transaction::BchTx(BchTx::default()));
+ let dash = sign_tx("BTC", Transaction::DashTx(DashTx::default()));
+ let ltc = sign_tx("DOGE", Transaction::LtcTx(LtcTx::default()));
+ assert!(is_supported_legacy_utxo_transaction(&bch));
+ assert!(is_supported_legacy_utxo_transaction(&dash));
+ assert!(is_supported_legacy_utxo_transaction(<c));
+ }
+
+ #[test]
+ fn bitcoin_and_dogecoin_variants_are_rejected_regardless_of_coin_code() {
+ let btc = sign_tx("LTC", Transaction::BtcTx(BtcTx::default()));
+ let doge = sign_tx("BCH", Transaction::DogeTx(DogeTx::default()));
+
+ assert!(is_legacy_utxo_transaction(&btc));
+ assert!(is_legacy_utxo_transaction(&doge));
+ assert!(!is_supported_legacy_utxo_transaction(&btc));
+ assert!(!is_supported_legacy_utxo_transaction(&doge));
+ }
+}
### rust/apps/bitcoin/src/transactions/mod.rs
@@ -1,3 +1,10 @@
+/// Deprecated raw-protobuf UTXO transaction implementation.
+///
+/// Retained only for BCH, DASH and LTC compatibility. Bitcoin transactions
+/// must use PSBT; other legacy UTXO variants are rejected at the product entry.
+#[deprecated(
+ note = "raw-protobuf Bitcoin transactions are deprecated; use PSBT for Bitcoin"
+)]
pub mod legacy;
pub mod parsed_tx;
pub mod psbt;
### rust/rust_c/src/bitcoin/legacy.rs
@@ -8,6 +8,15 @@ use crate::common::ur::{QRCodeType, UREncodeResult};
use crate::extract_array;
use alloc::boxed::Box;
use alloc::string::ToString;
+use ur_registry::pb::protoc;
+
+fn is_supported_legacy_utxo_payload(payload: &protoc::Payload) -> bool {
+ matches!(
+ payload.content.as_ref(),
+ Some(protoc::payload::Content::SignTx(sign_tx))
+ if app_bitcoin::network::is_supported_legacy_utxo_transaction(sign_tx)
+ )
+}
#[no_mangle]
pub unsafe extern "C" fn utxo_parse_keystone(
@@ -29,6 +38,12 @@ pub unsafe extern "C" fn utxo_parse_keystone(
build_payload(ptr, ur_type).map_or_else(
|e| TransactionParseResult::from(e).c_ptr(),
|payload| {
+ if !is_supported_legacy_utxo_payload(&payload) {
+ return TransactionParseResult::from(RustCError::UnsupportedTransaction(
+ app_bitcoin::network::UNSUPPORTED_LEGACY_UTXO_MESSAGE.to_string(),
+ ))
+ .c_ptr();
+ }
build_parse_context(master_fingerprint, x_pub).map_or_else(
|e| TransactionParseResult::from(e).c_ptr(),
|context| {
### rust/rust_c/src/common/keystone.rs
@@ -70,9 +70,23 @@ unsafe fn get_signed_tx(
x_pub: PtrString,
seed: &[u8],
) -> Result<(String, String), KeystoneError> {
+ #[cfg(feature = "bitcoin")]
+ let legacy_utxo_policy = payload.content.as_ref().and_then(|content| match content {
+ payload::Content::SignTx(sign_tx) => Some((
+ app_bitcoin::network::is_legacy_utxo_transaction(sign_tx),
+ app_bitcoin::network::is_supported_legacy_utxo_transaction(sign_tx),
+ )),
+ _ => None,
+ });
+ #[cfg(feature = "bitcoin")]
+ if matches!(legacy_utxo_policy, Some((true, false))) {
+ return Err(KeystoneError::SignTxFailed(
+ app_bitcoin::network::UNSUPPORTED_LEGACY_UTXO_MESSAGE.to_string(),
+ ));
+ }
build_parse_context(master_fingerprint, x_pub).and_then(|context| {
#[cfg(feature = "bitcoin")]
- if app_bitcoin::network::Network::from_str(coin_code.as_str()).is_ok() {
+ if matches!(legacy_utxo_policy, Some((true, true))) {
return app_bitcoin::sign_raw_tx(payload, context, seed)
.map_err(|e| KeystoneError::SignTxFailed(e.to_string()));
}
@@ -97,11 +111,21 @@ pub unsafe fn build_check_result(
let payload_content = payload.content.clone();
match payload_content {
Some(payload::Content::SignTx(sign_tx_content)) => {
+ #[cfg(feature = "bitcoin")]
+ let is_legacy_utxo =
+ app_bitcoin::network::is_legacy_utxo_transaction(&sign_tx_content);
+ #[cfg(feature = "bitcoin")]
+ let is_supported_legacy_utxo =
+ app_bitcoin::network::is_supported_legacy_utxo_transaction(&sign_tx_content);
+ #[cfg(feature = "bitcoin")]
+ if is_legacy_utxo && !is_supported_legacy_utxo {
+ return Err(KeystoneError::CheckTxFailed(
+ app_bitcoin::network::UNSUPPORTED_LEGACY_UTXO_MESSAGE.to_string(),
+ ));
+ }
build_parse_context(master_fingerprint, x_pub).and_then(|context| {
#[cfg(feature = "bitcoin")]
- if app_bitcoin::network::Network::from_str(sign_tx_content.coin_code.as_str())
- .is_ok()
- {
+ if is_supported_legacy_utxo {
return app_bitcoin::check_raw_tx(payload, context)
.map_err(|e| KeystoneError::CheckTxFailed(e.to_string()));
}
### rust/rust_c/src/common/ur_ext.rs
@@ -261,19 +261,26 @@ fn get_view_type_from_keystone(bytes: Vec<u8>) -> Result<ViewType, URError> {
.ok_or(URError::NotSupportURTypeError("empty payload".to_string()))?;
let result = match payload.content {
Some(protoc::payload::Content::SignTx(sign_tx_content)) => {
+ #[cfg(feature = "bitcoin")]
+ if app_bitcoin::network::is_legacy_utxo_transaction(&sign_tx_content) {
+ if !app_bitcoin::network::is_supported_legacy_utxo_transaction(&sign_tx_content) {
+ return Err(URError::NotSupportURTypeError(
+ app_bitcoin::network::UNSUPPORTED_LEGACY_UTXO_MESSAGE.to_string(),
+ ));
+ }
+ return match sign_tx_content.transaction.as_ref() {
+ #[cfg(feature = "bch")]
+ Some(protoc::sign_transaction::Transaction::BchTx(_)) => Ok(ViewType::BchTx),
+ #[cfg(feature = "dash")]
+ Some(protoc::sign_transaction::Transaction::DashTx(_)) => Ok(ViewType::DashTx),
+ #[cfg(feature = "ltc")]
+ Some(protoc::sign_transaction::Transaction::LtcTx(_)) => Ok(ViewType::LtcTx),
+ _ => Err(URError::NotSupportURTypeError(
+ app_bitcoin::network::UNSUPPORTED_LEGACY_UTXO_MESSAGE.to_string(),
+ )),
+ };
+ }
match sign_tx_content.coin_code.as_str() {
- "BTC_NATIVE_SEGWIT" => ViewType::BtcNativeSegwitTx,
- "BTC_SEGWIT" => ViewType::BtcSegwitTx,
- "BTC_LEGACY" => ViewType::BtcLegacyTx,
- "BTC" => ViewType::BtcSegwitTx,
- #[cfg(feature = "ltc")]
- "LTC" => ViewType::LtcTx,
- #[cfg(feature = "doge")]
- "DOGE" => ViewType::DogeTx,
- #[cfg(feature = "dash")]
- "DASH" => ViewType::DashTx,
- #[cfg(feature = "bch")]
- "BCH" => ViewType::BchTx,
#[cfg(feature = "ethereum")]
"ETH" => ViewType::EthTx,
#[cfg(feature = "tron")]Why this scored 61/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.