Refactor Sui module for improved error handling and memory management. Introduce `extract_array_mut` macro for mutable array extraction, enhance error messages in address generation and intent parsing, and ensure proper handling of empty paths in signing functions. Update GUI functions to streamline
What changed, and why it matters
This commit refactors the Sui blockchain support in the Keystone 3 hardware wallet firmware. The main security-relevant changes are: replacing several panic-prone `.unwrap()` calls with proper error handling, adding checks for empty derivation paths that previously could have caused crashes or undefined behavior, validating public key length during address generation, and ensuring the wallet's seed is wiped from memory (zeroized) more reliably after signing. The commit also fixes a memory-freeing bug in the user interface where the wrong type of result could be freed depending on whether a normal transaction or a 'sign message hash' request was being handled.
Treat this as a defensive hardening/refactoring patch. Review that the new `extract_array_mut` macro is only used with valid, non-null, correctly-sized pointers, because it performs unchecked `from_raw_parts_mut`. Verify that all call sites now zeroize seeds and that the GUI's `g_isSignHashRequest` flag is reset consistently to prevent double-free or wrong-free issues. No immediate incident response is indicated, but regression testing of Sui transaction and sign-hash flows is recommended.
Security signals we found
Replaced multiple `.unwrap()` calls with explicit error propagation in cryptographic operations
Added empty derivation path checks before indexing `paths[0]`
Added public key length validation in address generation
Added `zeroize` of seed material on both success and error paths in signing functions
Fixed potential use of wrong free function for parsed result type in GUI code
Added `extract_array_mut` macro for mutable pointer-to-slice conversion
Evidence from the diff
The patch hardens Rust code in rust/apps/sui/src/lib.rs and rust/rust_c/src/sui/mod.rs. generate_address now returns errors instead of unwrapping Blake2bVar::new/finalize_variable and validates the decoded public key is exactly 32 bytes. parse_intent rejects empty input. The rust_c Sui FFI functions (sui_check_request, sui_check_sign_hash_request, sui_parse_sign_message_hash, sui_sign_hash, sui_sign_intent) now check whether get_derivation_paths() is empty before indexing [0], preventing out-of-bounds/panic conditions. Signing functions use a new extract_array_mut macro and zeroize to clear the seed after use, including on error paths. sui_sign_hash now handles invalid hex in the message hash instead of unwrapping. The C GUI layer (gui_sui.c) tracks whether the current request is a sign-hash request so it calls the correct free function, and consolidates signing into SuiSignInternal with explicit seed clearing via memset_s.
Changed components
rust/apps/sui/src/lib.rsrust/rust_c/src/sui/mod.rsrust/rust_c/src/common/macros.rssrc/ui/gui_chain/multi/web3/gui_sui.cInspect captured patch +209 / −103
diff --git a/rust/apps/sui/src/lib.rs b/rust/apps/sui/src/lib.rs
index 51b9d46..c373f21 100644
--- a/rust/apps/sui/src/lib.rs
+++ b/rust/apps/sui/src/lib.rs
@@ -7,27 +7,22 @@ extern crate core;
#[macro_use]
extern crate std;
+use crate::{
+ errors::{Result, SuiError},
+ types::intent::IntentScope,
+};
use alloc::{
string::{String, ToString},
vec::Vec,
};
+use blake2::{
+ digest::{Update, VariableOutput},
+ Blake2bVar,
+};
use core::str::FromStr;
-
use serde_derive::{Deserialize, Serialize};
use sui_types::{message::PersonalMessage, transaction::TransactionData};
-
-use errors::SuiError;
use types::{intent::IntentMessage, msg::PersonalMessageUtf8};
-use {bcs, hex};
-use {
- blake2::{
- digest::{Update, VariableOutput},
- Blake2bVar,
- },
- serde_json,
-};
-
-use crate::{errors::Result, types::intent::IntentScope};
pub mod errors;
pub mod types;
@@ -40,17 +35,29 @@ pub enum Intent {
}
pub fn generate_address(pub_key: &str) -> Result<String> {
- let mut hasher = Blake2bVar::new(32).unwrap();
+ let mut hasher = Blake2bVar::new(32)
+ .map_err(|e| SuiError::InvalidData(format!("blake2b new failed: {e}")))?;
let mut buf: Vec<u8> = hex::decode(pub_key)?;
+ let buf_len = buf.len();
+ if buf_len != 32 {
+ return Err(SuiError::InvalidData(format!(
+ "invalid public key length: {buf_len}"
+ )));
+ }
// insert flag, ed25519 is 0, secp256k1 is 1, secp256r1 is 2, multi sign is 3.
buf.insert(0, 0);
hasher.update(&buf);
let mut addr = [0u8; 32];
- hasher.finalize_variable(&mut addr).unwrap();
+ hasher
+ .finalize_variable(&mut addr)
+ .map_err(|e| SuiError::InvalidData(format!("blake2b finalize failed: {e}")))?;
Ok(format!("0x{}", hex::encode(addr)))
}
pub fn parse_intent(intent: &[u8]) -> Result<Intent> {
+ if intent.is_empty() {
+ return Err(SuiError::InvalidData("intent is empty".to_string()));
+ }
match IntentScope::try_from(intent[0])? {
IntentScope::TransactionData | IntentScope::TransactionEffects => {
let tx: IntentMessage<TransactionData> =
@@ -62,11 +69,11 @@ pub fn parse_intent(intent: &[u8]) -> Result<Intent> {
Ok(msg) => msg,
Err(_) => {
if intent.len() < 4 {
- return Err(SuiError::InvalidData(String::from("message too short")));
+ return Err(SuiError::InvalidData("message too short".to_string()));
}
- let intent_bytes = intent[..3].to_vec();
+ let intent_bytes = &intent[..3];
IntentMessage::<PersonalMessage>::new(
- types::intent::Intent::from_str(hex::encode(intent_bytes).as_str())?,
+ types::intent::Intent::from_str(&hex::encode(intent_bytes))?,
PersonalMessage {
message: intent[3..].to_vec(),
},
@@ -84,7 +91,7 @@ pub fn parse_intent(intent: &[u8]) -> Result<Intent> {
value: PersonalMessageUtf8 { message: m },
}))
}
- _ => Err(SuiError::InvalidData(String::from("unsupported intent"))),
+ _ => Err(SuiError::InvalidData("unsupported intent".to_string())),
}
}
@@ -92,7 +99,7 @@ pub fn decode_utf8(msg: &[u8]) -> Result<String> {
match String::from_utf8(msg.to_vec()) {
Ok(utf8_msg) => {
if app_utils::is_cjk(&utf8_msg) {
- Err(errors::SuiError::InvalidData(String::from("contains CJK")))
+ Err(errors::SuiError::InvalidData("contains CJK".to_string()))
} else {
Ok(utf8_msg)
}
@@ -102,10 +109,13 @@ pub fn decode_utf8(msg: &[u8]) -> Result<String> {
}
pub fn sign_intent(seed: &[u8], path: &String, intent: &[u8]) -> Result<[u8; 64]> {
- let mut hasher = Blake2bVar::new(32).unwrap();
+ let mut hasher = Blake2bVar::new(32)
+ .map_err(|e| SuiError::InvalidData(format!("blake2b new failed: {e}")))?;
hasher.update(intent);
let mut hash = [0u8; 32];
- hasher.finalize_variable(&mut hash).unwrap();
+ hasher
+ .finalize_variable(&mut hash)
+ .map_err(|e| SuiError::InvalidData(format!("blake2b finalize failed: {e}")))?;
let sig =
keystore::algorithms::ed25519::slip10_ed25519::sign_message_by_seed(seed, path, &hash)
.map_err(|e| errors::SuiError::SignFailure(e.to_string()))?;
diff --git a/rust/rust_c/src/common/macros.rs b/rust/rust_c/src/common/macros.rs
index 514702f..b43a89d 100644
--- a/rust/rust_c/src/common/macros.rs
+++ b/rust/rust_c/src/common/macros.rs
@@ -490,6 +490,16 @@ macro_rules! extract_array {
}};
}
+#[macro_export]
+macro_rules! extract_array_mut {
+ ($x: expr, $name: ident, $length: expr) => {{
+ let ptr = $x as *mut $name;
+ let result: &mut [$name] = core::slice::from_raw_parts_mut(ptr, $length as usize);
+ result
+ }};
+}
+
+
#[macro_export]
macro_rules! impl_response {
($name:ident) => {
diff --git a/rust/rust_c/src/sui/mod.rs b/rust/rust_c/src/sui/mod.rs
index 06f42e5..0fd5671 100644
--- a/rust/rust_c/src/sui/mod.rs
+++ b/rust/rust_c/src/sui/mod.rs
@@ -13,12 +13,12 @@ use crate::common::structs::{SimpleResponse, TransactionCheckResult, Transaction
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::{extract_array, extract_array_mut, extract_ptr_with_type};
use app_sui::errors::SuiError;
use app_utils::normalize_path;
use structs::DisplaySuiIntentMessage;
use structs::DisplaySuiSignMessageHash;
+use zeroize::Zeroize;
pub mod structs;
@@ -47,7 +47,14 @@ pub unsafe extern "C" fn sui_check_request(
}
let mfp = extract_array!(master_fingerprint, u8, 4);
let sign_request = extract_ptr_with_type!(ptr, SuiSignRequest);
- let ur_mfp = sign_request.get_derivation_paths()[0].get_source_fingerprint();
+
+ let paths = sign_request.get_derivation_paths();
+ if paths.is_empty() {
+ return TransactionCheckResult::from(RustCError::InvalidHDPath).c_ptr();
+ }
+
+ // According to SDK convention, index 0 is the signing path
+ let ur_mfp = paths[0].get_source_fingerprint();
if let Ok(mfp) = mfp.try_into() as Result<[u8; 4], _> {
if let Some(ur_mfp) = ur_mfp {
@@ -74,7 +81,14 @@ pub unsafe extern "C" fn sui_check_sign_hash_request(
}
let mfp = extract_array!(master_fingerprint, u8, 4);
let sign_hash_request = extract_ptr_with_type!(ptr, SuiSignHashRequest);
- let ur_mfp = sign_hash_request.get_derivation_paths()[0].get_source_fingerprint();
+
+ let paths = sign_hash_request.get_derivation_paths();
+ if paths.is_empty() {
+ return TransactionCheckResult::from(RustCError::InvalidHDPath).c_ptr();
+ }
+
+ // According to SDK convention, index 0 is the signing path
+ let ur_mfp = paths[0].get_source_fingerprint();
if let Ok(mfp) = mfp.try_into() as Result<[u8; 4], _> {
if let Some(ur_mfp) = ur_mfp {
@@ -111,68 +125,135 @@ pub unsafe extern "C" fn sui_parse_intent(
Err(e) => TransactionParseResult::from(e).c_ptr(),
}
}
+
#[no_mangle]
pub unsafe extern "C" fn sui_parse_sign_message_hash(
ptr: PtrUR,
) -> PtrT<TransactionParseResult<DisplaySuiSignMessageHash>> {
let sign_hash_request = extract_ptr_with_type!(ptr, SuiSignHashRequest);
let message = sign_hash_request.get_message_hash();
- let path = sign_hash_request.get_derivation_paths()[0].get_path();
+
+ 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 = "Sui".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(
DisplaySuiSignMessageHash::new(
network,
- path.unwrap_or("No Path".to_string()),
+ path,
message,
- hex::encode(address[0].clone()),
+ address_hex,
)
.c_ptr(),
)
.c_ptr()
}
+unsafe fn sui_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_sui_signature_result(
+ seed: &mut [u8],
+ request_id: Option<Vec<u8>>,
+ signature: [u8; 64],
+ pub_key: Vec<u8>,
+) -> PtrT<UREncodeResult> {
+ let sig = SuiSignature::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,
+ SuiSignature::get_registry_type().get_type(),
+ FRAGMENT_MAX_LENGTH_DEFAULT,
+ )
+ .c_ptr()
+}
+
#[no_mangle]
pub unsafe extern "C" fn sui_sign_hash(
ptr: PtrUR,
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, SuiSignHashRequest);
- let hash = sign_request.get_message_hash();
- 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()
}
};
- 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 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 pub_key = match get_public_key(seed, &path) {
- Ok(v) => v,
- Err(e) => return UREncodeResult::from(e).c_ptr(),
+
+ let (signature, pub_key) = match sui_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(),
};
- let sig = SuiSignature::new(
+
+ build_sui_signature_result(
+ seed,
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(),
- };
- UREncodeResult::encode(
- sig_data,
- SuiSignature::get_registry_type().get_type(),
- FRAGMENT_MAX_LENGTH_DEFAULT,
+ signature,
+ pub_key,
)
- .c_ptr()
}
#[no_mangle]
@@ -181,39 +262,38 @@ pub unsafe extern "C" fn sui_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, SuiSignRequest);
- 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()
}
};
- 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 sign_data = sign_request.get_intent_message();
+
+ let (signature, pub_key) = match sui_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(),
};
- let sig = SuiSignature::new(
+
+ build_sui_signature_result(
+ seed,
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(),
- };
- UREncodeResult::encode(
- sig_data,
- SuiSignature::get_registry_type().get_type(),
- FRAGMENT_MAX_LENGTH_DEFAULT,
+ signature,
+ pub_key,
)
- .c_ptr()
}
diff --git a/src/ui/gui_chain/multi/web3/gui_sui.c b/src/ui/gui_chain/multi/web3/gui_sui.c
index 6389f55..8ee4ba1 100644
--- a/src/ui/gui_chain/multi/web3/gui_sui.c
+++ b/src/ui/gui_chain/multi/web3/gui_sui.c
@@ -12,6 +12,7 @@ static bool g_isMulti = false;
static URParseResult *g_urResult = NULL;
static URParseMultiResult *g_urMultiResult = NULL;
static void *g_parseResult = NULL;
+static bool g_isSignHashRequest = false;
void GuiSetSuiUrData(URParseResult *urResult, URParseMultiResult *urMultiResult, bool multi)
{
@@ -21,15 +22,21 @@ void GuiSetSuiUrData(URParseResult *urResult, URParseMultiResult *urMultiResult,
}
#define CHECK_FREE_PARSE_RESULT(result) \
- if (result != NULL) \
- { \
- free_TransactionParseResult_DisplaySuiIntentMessage((PtrT_TransactionParseResult_DisplaySuiIntentMessage)result); \
- result = NULL; \
- }
+ do { \
+ if (result != NULL) { \
+ if (g_isSignHashRequest) { \
+ free_TransactionParseResult_DisplaySuiSignMessageHash((PtrT_TransactionParseResult_DisplaySuiSignMessageHash)result); \
+ } else { \
+ free_TransactionParseResult_DisplaySuiIntentMessage((PtrT_TransactionParseResult_DisplaySuiIntentMessage)result); \
+ } \
+ result = NULL; \
+ } \
+ } while (0)
void *GuiGetSuiData(void)
{
CHECK_FREE_PARSE_RESULT(g_parseResult);
+ g_isSignHashRequest = false;
void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
do {
PtrT_TransactionParseResult_DisplaySuiIntentMessage parseResult = sui_parse_intent(data);
@@ -42,6 +49,7 @@ void *GuiGetSuiData(void)
void *GuiGetSuiSignMessageHashData(void)
{
CHECK_FREE_PARSE_RESULT(g_parseResult);
+ g_isSignHashRequest = true;
void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
do {
PtrT_TransactionParseResult_DisplaySuiSignMessageHash parseResult = sui_parse_sign_message_hash(data);
@@ -185,8 +193,6 @@ void GuiShowSuiSignMessageHashOverview(lv_obj_t *parent, void *totalData)
lv_obj_align_to(message_hash_value, message_hash_label, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 8);
}
-
-
void GuiShowSuiSignMessageHashDetails(lv_obj_t *parent, void *totalData)
{
lv_obj_set_size(parent, 408, 444);
@@ -240,8 +246,6 @@ void GuiShowSuiSignMessageHashDetails(lv_obj_t *parent, void *totalData)
lv_label_set_long_mode(message_hash_notice_content, LV_LABEL_LONG_WRAP);
}
-
-
void FreeSuiMemory(void)
{
CHECK_FREE_UR_RESULT(g_urResult, false);
@@ -262,38 +266,40 @@ void GetSuiDetail(void *indata, void *param, uint32_t maxLen)
strcpy((char *)indata, tx->detail);
}
-UREncodeResult *GuiGetSuiSignQrCodeData(void)
+static UREncodeResult *SuiSignInternal(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 = sui_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 *GuiGetSuiSignQrCodeData(void)
+{
+ void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+ return SuiSignInternal(sui_sign_intent, data);
+}
+
UREncodeResult *GuiGetSuiSignHashQrCodeData(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 = sui_sign_hash(data, seed, len);
- ClearSecretCache();
- CHECK_CHAIN_BREAK(encodeResult);
- } while (0);
- SetLockScreen(enable);
- return encodeResult;
+ return SuiSignInternal(sui_sign_hash, data);
}
\ No newline at end of file
Why this scored 44/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.