What changed, and why it matters
This commit reviews and hardens the IOTA cryptocurrency support in the Keystone 3 hardware wallet firmware. It removes unused dependencies, fixes several places where the code would crash if given empty or malformed input, and improves handling of the secret seed so it is wiped from memory even when an error occurs. There is no explicit statement from the vendor that this fixes a security vulnerability, but the changes reduce the chance of a crash or information leak when signing IOTA transactions.
Treat this as a defensive hardening patch. Reviewers should verify that all early-return paths in iota_sign_hash and iota_sign_intent now zeroize the seed, and that the C caller's memset_s is not optimized away. Users should update firmware once this commit is included in a release, especially if they use IOTA signing.
Security signals we found
Memory safety: seed buffer is now zeroized on all error paths in iota_sign_hash and iota_sign_intent
Input validation: empty derivation path arrays are checked before indexing
Input validation: hex decoding of message hash no longer uses unwrap()
Input validation: missing addresses no longer cause out-of-bounds access
Dead code removal: duplicate iota_get_address removed from solana module
Dependency reduction: unused cryptographic/serialization crates removed from iota app
C layer hardening: GetAccountSeed return value checked and seed cleared with memset_s
Evidence from the diff
The patch refactors the IOTA Rust/C integration. Key changes: (1) Cargo dependencies are trimmed, removing unused crates such as bech32, blake2, bytes, serde, ur-registry, app_utils, rust_tools. (2) FFI functions iota_sign_hash and iota_sign_intent now use extract_array_mut! and call seed.zeroize() on every error path, preventing the BIP39 seed from lingering in memory after failures. (3) Empty derivation-path arrays are now checked instead of panicking on index [0]. (4) Hex decoding of the message hash is now validated instead of unwrap(). (5) Address extraction in iota_parse_intent and iota_parse_sign_message_hash handles missing addresses gracefully. (6) A duplicate iota_get_address binding is removed from the Solana module. (7) The C layer now checks GetAccountSeed return value and clears the local seed buffer with memset_s. (8) A bridge-transaction recipient extraction no longer unwraps an invalid checksum address. These are defensive hardening fixes rather than a single obvious exploit.
Changed components
rust/apps/iotarust/rust_c/src/iota/mod.rsrust/rust_c/src/iota/structs.rsrust/rust_c/src/solana/mod.rssrc/ui/gui_chain/multi/web3/gui_iota.cInspect captured patch +203 / −117
diff --git a/rust/Cargo.lock b/rust/Cargo.lock
index 062f882..7a8e85b 100644
--- a/rust/Cargo.lock
+++ b/rust/Cargo.lock
@@ -276,17 +276,10 @@ dependencies = [
name = "app_iota"
version = "0.1.0"
dependencies = [
- "app_utils",
- "bech32 0.11.0",
- "blake2",
- "bytes",
"cryptoxide",
"hex",
"keystore",
- "rust_tools",
- "serde",
"thiserror-core",
- "ur-registry",
]
[[package]]
diff --git a/rust/apps/iota/Cargo.toml b/rust/apps/iota/Cargo.toml
index 4d46023..c27e8e1 100644
--- a/rust/apps/iota/Cargo.toml
+++ b/rust/apps/iota/Cargo.toml
@@ -9,14 +9,4 @@ edition = "2021"
keystore = { workspace = true, default-features = false }
hex = { workspace = true }
cryptoxide = { workspace = true }
-ur-registry = { workspace = true }
-app_utils = { workspace = true }
-rust_tools = { workspace = true }
thiserror = { workspace = true }
-bytes = {version = "1.4.0", default-features = false}
-serde = { workspace = true }
-blake2 = { workspace = true }
-
-[dev-dependencies]
-keystore = { workspace = true }
-bech32 = { workspace = true }
diff --git a/rust/apps/iota/README.md b/rust/apps/iota/README.md
new file mode 100644
index 0000000..4c6d1e8
--- /dev/null
+++ b/rust/apps/iota/README.md
@@ -0,0 +1,9 @@
+# IOTA Implementation
+
+IOTA uses the same underlying Move VM as Sui, therefore this implementation
+reuses Sui's core functionality for transaction parsing and signing.
+
+The main differences are:
+- IOTA-specific address generation (Blake2b-256)
+- IOTA-specific network parameters
+- Custom transaction display formatting for IOTA ecosystem
\ No newline at end of file
diff --git a/rust/apps/iota/src/address.rs b/rust/apps/iota/src/address.rs
index 7700dfe..6e1fc62 100644
--- a/rust/apps/iota/src/address.rs
+++ b/rust/apps/iota/src/address.rs
@@ -19,6 +19,7 @@ pub fn get_address_from_pubkey(pubkey: String) -> Result<String> {
mod tests {
use super::*;
use alloc::string::ToString;
+ use cryptoxide::hashing::blake2b_256;
#[test]
fn test_get_address_from_pubkey() {
@@ -29,4 +30,54 @@ mod tests {
"0x193a4811b7207ac7a861f840552f9c718172400f4c46bdef5935008a7977fb04"
);
}
+
+ #[test]
+ fn test_get_address_from_pubkey_invalid_hex() {
+ let pubkey = "zz"; // invalid hex
+ let address = get_address_from_pubkey(pubkey.to_string());
+ assert!(matches!(address, Err(IotaError::InvalidData(_))));
+ }
+
+ #[test]
+ fn test_get_address_from_pubkey_with_prefix() {
+ let pubkey = "0xbfa73107effa14b21ff1b9ae2e6b2e770232b7c29018abbf76475b25395369c0";
+ let address = get_address_from_pubkey(pubkey.to_string());
+ assert!(matches!(address, Err(IotaError::InvalidData(_))));
+ }
+
+ #[test]
+ fn test_get_address_from_pubkey_empty() {
+ let pubkey = "";
+ let address = get_address_from_pubkey(pubkey.to_string());
+ assert!(
+ matches!(address, Err(IotaError::InvalidData(msg)) if msg.contains("pubkey is not 32 bytes"))
+ );
+ }
+
+ #[test]
+ fn test_get_address_from_pubkey_len_31() {
+ // 31 bytes (62 hex chars)
+ let pubkey = "aa".repeat(31);
+ let address = get_address_from_pubkey(pubkey);
+ assert!(
+ matches!(address, Err(IotaError::InvalidData(msg)) if msg.contains("pubkey is not 32 bytes"))
+ );
+ }
+
+ #[test]
+ fn test_get_address_from_pubkey_case_insensitive() {
+ let lower = "bfa73107effa14b21ff1b9ae2e6b2e770232b7c29018abbf76475b25395369c0";
+ let upper = lower.to_uppercase();
+ let addr_lower = get_address_from_pubkey(lower.to_string()).unwrap();
+ let addr_upper = get_address_from_pubkey(upper);
+ assert_eq!(addr_lower, addr_upper.unwrap());
+ }
+
+ #[test]
+ fn test_get_address_from_pubkey_output_format() {
+ let pubkey = "bfa73107effa14b21ff1b9ae2e6b2e770232b7c29018abbf76475b25395369c0";
+ let addr = get_address_from_pubkey(pubkey.to_string()).unwrap();
+ assert!(addr.starts_with("0x"));
+ assert_eq!(addr.len(), 66);
+ }
}
diff --git a/rust/rust_c/src/iota/mod.rs b/rust/rust_c/src/iota/mod.rs
index acf8e8d..3a7c3df 100644
--- a/rust/rust_c/src/iota/mod.rs
+++ b/rust/rust_c/src/iota/mod.rs
@@ -1,11 +1,12 @@
+use crate::common::errors::RustCError;
use crate::common::structs::SimpleResponse;
use crate::common::structs::{TransactionCheckResult, TransactionParseResult};
use crate::common::types::{PtrBytes, PtrString, PtrT, PtrUR};
use crate::common::ur::{UREncodeResult, FRAGMENT_MAX_LENGTH_DEFAULT};
use crate::common::utils::{convert_c_char, recover_c_char};
-use crate::extract_array;
use crate::extract_ptr_with_type;
use crate::sui::get_public_key;
+use crate::{extract_array, extract_array_mut};
use alloc::format;
use alloc::vec::Vec;
use alloc::{
@@ -14,8 +15,6 @@ use alloc::{
};
use app_sui::errors::SuiError;
use app_sui::Intent;
-use bitcoin::bip32::DerivationPath;
-use core::str::FromStr;
use cty::c_char;
use structs::DisplayIotaIntentData;
use structs::DisplayIotaSignMessageHash;
@@ -24,6 +23,7 @@ use ur_registry::iota::{
iota_sign_hash_request::IotaSignHashRequest, iota_sign_request::IotaSignRequest,
};
use ur_registry::traits::RegistryItem;
+use zeroize::Zeroize;
pub mod structs;
@@ -44,10 +44,13 @@ pub unsafe extern "C" fn iota_parse_intent(
) -> PtrT<TransactionParseResult<DisplayIotaIntentData>> {
let sign_request = extract_ptr_with_type!(ptr, IotaSignRequest);
let sign_data = sign_request.get_intent_message();
- let address = format!(
- "0x{}",
- hex::encode(sign_request.get_addresses().unwrap_or(vec![])[0].clone())
- );
+
+ let address = sign_request
+ .get_addresses()
+ .and_then(|addrs| addrs.first().cloned())
+ .map(|addr| format!("0x{}", hex::encode(addr)))
+ .unwrap_or_else(|| "0x".to_string());
+
let data = app_sui::parse_intent(&sign_data);
match data {
Ok(data) => match data {
@@ -73,17 +76,72 @@ pub unsafe extern "C" fn iota_parse_sign_message_hash(
) -> PtrT<TransactionParseResult<DisplayIotaSignMessageHash>> {
let sign_hash_request = extract_ptr_with_type!(ptr, IotaSignHashRequest);
let message = sign_hash_request.get_message_hash();
- let path = sign_hash_request.get_derivation_paths()[0].get_path();
+
+ // Check derivation paths is not empty
+ let paths = sign_hash_request.get_derivation_paths();
+ let path = if paths.is_empty() {
+ "No Path".to_string()
+ } else {
+ paths[0].get_path().unwrap_or("No Path".to_string())
+ };
+
let network = "IOTA".to_string();
let address = sign_hash_request.get_addresses().unwrap_or(vec![]);
+ let address_hex = if address.is_empty() {
+ "".to_string()
+ } else {
+ hex::encode(&address[0])
+ };
+
TransactionParseResult::success(
- DisplayIotaSignMessageHash::new(
- network,
- path.unwrap_or("No Path".to_string()),
- message,
- hex::encode(address[0].clone()),
- )
- .c_ptr(),
+ DisplayIotaSignMessageHash::new(network, path, message, address_hex).c_ptr(),
+ )
+ .c_ptr()
+}
+
+unsafe fn iota_sign_internal<F>(
+ seed: &mut [u8],
+ path: &str,
+ sign_fn: F,
+) -> Result<([u8; 64], Vec<u8>), UREncodeResult>
+where
+ F: FnOnce(&[u8], &str) -> Result<[u8; 64], SuiError>,
+{
+ let signature = sign_fn(seed, path).map_err(|e| {
+ seed.zeroize();
+ UREncodeResult::from(e)
+ })?;
+
+ let pub_key = get_public_key(seed, &path.to_string()).map_err(|e| {
+ seed.zeroize();
+ UREncodeResult::from(e)
+ })?;
+
+ Ok((signature, pub_key))
+}
+
+unsafe fn build_iota_signature_result(
+ seed: &mut [u8],
+ request_id: Option<Vec<u8>>,
+ signature: [u8; 64],
+ pub_key: Vec<u8>,
+) -> PtrT<UREncodeResult> {
+ let sig = IotaSignature::new(request_id, signature.to_vec(), Some(pub_key));
+
+ let sig_data: Vec<u8> = match sig.try_into() {
+ Ok(v) => v,
+ Err(e) => {
+ seed.zeroize();
+ return UREncodeResult::from(e).c_ptr();
+ }
+ };
+
+ seed.zeroize();
+
+ UREncodeResult::encode(
+ sig_data,
+ IotaSignature::get_registry_type().get_type(),
+ FRAGMENT_MAX_LENGTH_DEFAULT,
)
.c_ptr()
}
@@ -94,41 +152,43 @@ pub unsafe extern "C" fn iota_sign_hash(
seed: PtrBytes,
seed_len: u32,
) -> PtrT<UREncodeResult> {
- let seed = extract_array!(seed, u8, seed_len as usize);
+ let mut seed = extract_array_mut!(seed, u8, seed_len as usize);
let sign_request = extract_ptr_with_type!(ptr, IotaSignHashRequest);
- let hash = sign_request.get_message_hash();
- let path = match sign_request.get_derivation_paths()[0].get_path() {
+
+ // Extract and validate path
+ let paths = sign_request.get_derivation_paths();
+ if paths.is_empty() {
+ seed.zeroize();
+ return UREncodeResult::from(RustCError::InvalidHDPath).c_ptr();
+ }
+ let path = match paths[0].get_path() {
Some(p) => p,
None => {
+ seed.zeroize();
return UREncodeResult::from(SuiError::SignFailure(
"invalid derivation path".to_string(),
))
- .c_ptr()
+ .c_ptr();
}
};
- let signature = match app_sui::sign_hash(seed, &path, &hex::decode(hash).unwrap()) {
- Ok(v) => v,
- Err(e) => return UREncodeResult::from(e).c_ptr(),
- };
- let pub_key = match get_public_key(seed, &path) {
- Ok(v) => v,
- Err(e) => return UREncodeResult::from(e).c_ptr(),
+
+ let hash = sign_request.get_message_hash();
+ let hash_bytes = match hex::decode(hash) {
+ Ok(bytes) => bytes,
+ Err(e) => {
+ seed.zeroize();
+ return UREncodeResult::from(RustCError::InvalidHex(e.to_string())).c_ptr();
+ }
};
- let sig = IotaSignature::new(
- sign_request.get_request_id(),
- signature.to_vec(),
- Some(pub_key),
- );
- let sig_data: Vec<u8> = match sig.try_into() {
- Ok(v) => v,
- Err(e) => return UREncodeResult::from(e).c_ptr(),
+
+ let (signature, pub_key) = match iota_sign_internal(seed, &path, |s, p| {
+ app_sui::sign_hash(s, &p.to_string(), &hash_bytes)
+ }) {
+ Ok(result) => result,
+ Err(err) => return err.c_ptr(),
};
- UREncodeResult::encode(
- sig_data,
- IotaSignature::get_registry_type().get_type(),
- FRAGMENT_MAX_LENGTH_DEFAULT,
- )
- .c_ptr()
+
+ build_iota_signature_result(seed, sign_request.get_request_id(), signature, pub_key)
}
#[no_mangle]
@@ -137,39 +197,33 @@ pub unsafe extern "C" fn iota_sign_intent(
seed: PtrBytes,
seed_len: u32,
) -> PtrT<UREncodeResult> {
- let seed = extract_array!(seed, u8, seed_len as usize);
+ let mut seed = extract_array_mut!(seed, u8, seed_len as usize);
let sign_request = extract_ptr_with_type!(ptr, IotaSignRequest);
- let sign_data = sign_request.get_intent_message();
- let path = match sign_request.get_derivation_paths()[0].get_path() {
+
+ let paths = sign_request.get_derivation_paths();
+ if paths.is_empty() {
+ seed.zeroize();
+ return UREncodeResult::from(RustCError::InvalidHDPath).c_ptr();
+ }
+ let path = match paths[0].get_path() {
Some(p) => p,
None => {
+ seed.zeroize();
return UREncodeResult::from(SuiError::SignFailure(
"invalid derivation path".to_string(),
))
- .c_ptr()
+ .c_ptr();
}
};
- let signature = match app_sui::sign_intent(seed, &path, &sign_data.to_vec()) {
- Ok(v) => v,
- Err(e) => return UREncodeResult::from(e).c_ptr(),
- };
- let pub_key = match get_public_key(seed, &path) {
- Ok(v) => v,
- Err(e) => return UREncodeResult::from(e).c_ptr(),
- };
- let sig = IotaSignature::new(
- sign_request.get_request_id(),
- signature.to_vec(),
- Some(pub_key),
- );
- let sig_data: Vec<u8> = match sig.try_into() {
- Ok(v) => v,
- Err(e) => return UREncodeResult::from(e).c_ptr(),
+
+ let sign_data = sign_request.get_intent_message();
+
+ let (signature, pub_key) = match iota_sign_internal(seed, &path, |s, p| {
+ app_sui::sign_intent(s, &p.to_string(), &sign_data.to_vec())
+ }) {
+ Ok(result) => result,
+ Err(err) => return err.c_ptr(),
};
- UREncodeResult::encode(
- sig_data,
- IotaSignature::get_registry_type().get_type(),
- FRAGMENT_MAX_LENGTH_DEFAULT,
- )
- .c_ptr()
+
+ build_iota_signature_result(seed, sign_request.get_request_id(), signature, pub_key)
}
diff --git a/rust/rust_c/src/iota/structs.rs b/rust/rust_c/src/iota/structs.rs
index 4733049..7e41901 100644
--- a/rust/rust_c/src/iota/structs.rs
+++ b/rust/rust_c/src/iota/structs.rs
@@ -168,11 +168,12 @@ fn extract_transaction_params(
let (recipient, to) = if method_string.contains("bridge") {
let to = pure_args
.iter()
+ // move transaction pure args
.find(|bytes| bytes.len() == (20 + 1 + 1 + 1))
- .map(|bytes| {
- convert_c_char(
- checksum_address(&format!("0x{}", hex::encode(&bytes[3..]))).unwrap(),
- )
+ .and_then(|bytes| {
+ checksum_address(&format!("0x{}", hex::encode(&bytes[3..])))
+ .ok()
+ .map(|addr| convert_c_char(addr))
})
.unwrap_or(null_mut());
(null_mut(), to)
diff --git a/rust/rust_c/src/solana/mod.rs b/rust/rust_c/src/solana/mod.rs
index b666a18..bb64d83 100644
--- a/rust/rust_c/src/solana/mod.rs
+++ b/rust/rust_c/src/solana/mod.rs
@@ -45,16 +45,6 @@ pub unsafe extern "C" fn solana_get_address(pubkey: PtrString) -> *mut SimpleRes
}
}
-#[no_mangle]
-pub unsafe extern "C" fn iota_get_address(pubkey: PtrString) -> *mut SimpleResponse<c_char> {
- let x_pub = recover_c_char(pubkey);
- let address = app_solana::get_address(&x_pub);
- match address {
- Ok(result) => SimpleResponse::success(convert_c_char(result) as *mut c_char).simple_c_ptr(),
- Err(e) => SimpleResponse::from(e).simple_c_ptr(),
- }
-}
-
#[no_mangle]
pub unsafe extern "C" fn solana_check(
ptr: PtrUR,
diff --git a/src/ui/gui_chain/multi/web3/gui_iota.c b/src/ui/gui_chain/multi/web3/gui_iota.c
index 47051ec..f9faadc 100644
--- a/src/ui/gui_chain/multi/web3/gui_iota.c
+++ b/src/ui/gui_chain/multi/web3/gui_iota.c
@@ -86,24 +86,34 @@ void FreeIotaMemory(void)
CHECK_FREE_PARSE_RESULT(g_parseResult);
}
-UREncodeResult *GuiGetIotaSignQrCodeData(void)
+static UREncodeResult *IotaSignInternal(UREncodeResult * (*sign_func)(void *, PtrBytes, uint32_t), void *data)
{
bool enable = IsPreviousLockScreenEnable();
SetLockScreen(false);
- UREncodeResult *encodeResult;
- void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+ UREncodeResult *encodeResult = NULL;
+ uint8_t seed[SEED_LEN] = {0};
+ int ret = 0;
do {
- uint8_t seed[64];
- GetAccountSeed(GetCurrentAccountIndex(), seed, SecretCacheGetPassword());
+ ret = GetAccountSeed(GetCurrentAccountIndex(), seed, SecretCacheGetPassword());
+ if (ret != 0) {
+ break;
+ }
int len = GetMnemonicType() == MNEMONIC_TYPE_BIP39 ? sizeof(seed) : GetCurrentAccountEntropyLen();
- encodeResult = iota_sign_intent(data, seed, len);
- ClearSecretCache();
+ encodeResult = sign_func(data, seed, len);
CHECK_CHAIN_BREAK(encodeResult);
} while (0);
+ memset_s(seed, sizeof(seed), 0, sizeof(seed));
+ ClearSecretCache();
SetLockScreen(enable);
return encodeResult;
}
+UREncodeResult *GuiGetIotaSignQrCodeData(void)
+{
+ void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+ return IotaSignInternal(iota_sign_intent, data);
+}
+
bool GetIotaIsTransaction(void *indata, void *param)
{
return !GetIotaIsMessage(indata, param);
@@ -201,18 +211,6 @@ void GuiIotaTxRawData(lv_obj_t *parent, void *totalData)
UREncodeResult *GuiGetIotaSignHashQrCodeData(void)
{
- bool enable = IsPreviousLockScreenEnable();
- SetLockScreen(false);
- UREncodeResult *encodeResult;
void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
- do {
- uint8_t seed[64];
- GetAccountSeed(GetCurrentAccountIndex(), seed, SecretCacheGetPassword());
- int len = GetMnemonicType() == MNEMONIC_TYPE_BIP39 ? sizeof(seed) : GetCurrentAccountEntropyLen();
- encodeResult = iota_sign_hash(data, seed, len);
- ClearSecretCache();
- CHECK_CHAIN_BREAK(encodeResult);
- } while (0);
- SetLockScreen(enable);
- return encodeResult;
+ return IotaSignInternal(iota_sign_hash, data);
}
\ No newline at end of file
Why this scored 47/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.