Merge pull request #2268 from KeystoneHQ/regular-review-fix
What changed, and why it matters
This commit tightens which old-style Bitcoin-like transactions the Keystone hardware wallet will accept. It now rejects raw protobuf Bitcoin (BTC) and Dogecoin (DOGE) legacy UTXO transactions, allowing only Bitcoin Cash (BCH), Dash (DASH), and Litecoin (LTC) to keep using that older path. Bitcoin must now go through the newer PSBT route. The change is framed as a deprecation/hardening fix rather than a response to a known exploit.
Treat as a hardening/deprecation change. Review whether any user-facing flows still rely on BTC or DOGE legacy UTXO transactions and confirm they are gracefully migrated to PSBT. Verify that the new rejection paths cannot be bypassed by malformed protobuf payloads or coin_code spoofing. No immediate incident response is indicated by the commit alone.
Security signals we found
Deprecation of raw-protobuf Bitcoin transaction handling
Rejection of unsupported legacy UTXO variants at multiple entry points
Shift from coin_code string matching to protobuf variant matching for legacy path eligibility
Bitcoin transactions now required to use PSBT path
Evidence from the diff
The patch adds helper functions is_legacy_utxo_transaction and is_supported_legacy_utxo_transaction in rust/apps/bitcoin/src/network.rs. It marks the legacy raw-protobuf transaction module as deprecated and inserts rejection checks in three C/Rust bridge points: utxo_parse_keystone, get_signed_tx/build_check_result, and get_view_type_from_keystone. If a legacy UTXO payload is BTC or DOGE, the firmware returns an ‘unsupported legacy UTXO transaction’ error instead of processing it. BCH, DASH, and LTC remain supported on the legacy path. The discriminator is the protobuf transaction variant, not the coin_code string, and tests are added to confirm that behavior.
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 46/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.