What changed, and why it matters
This commit only adds new unit tests to the Sui app in the Keystone 3 firmware. It does not change any production code, fix bugs, or alter behavior. The tests check things like UTF-8 decoding rules, address generation error cases, intent parsing, and signature determinism. There is no security issue in the commit itself.
No security action required. Treat as routine test-coverage improvement.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff is entirely additive test code in rust/apps/sui/src/lib.rs and rust/apps/sui/src/types/intent.rs. It introduces new #[test] functions covering decode_utf8 CJK rejection, emoji allowance, personal-message BCS roundtrips, generate_address validation, parse_intent short-input handling, Intent constructors, IntentScope/IntentVersion TryFrom behavior, Intent::from_str parsing, sign_intent determinism/path isolation, and normalize_path behavior. No production logic is modified.
Changed components
rust/apps/sui/src/lib.rsrust/apps/sui/src/types/intent.rsInspect captured patch +180 / −1
diff --git a/rust/apps/sui/src/lib.rs b/rust/apps/sui/src/lib.rs
index cd7cb27..08c2a7c 100644
--- a/rust/apps/sui/src/lib.rs
+++ b/rust/apps/sui/src/lib.rs
@@ -131,6 +131,7 @@ pub fn sign_hash(seed: &[u8], path: &String, hash: &[u8]) -> Result<[u8; 64]> {
mod tests {
use alloc::string::ToString;
+ use core::str::FromStr;
use serde_json::json;
use super::*;
@@ -193,6 +194,47 @@ mod tests {
assert_eq!(msg_str.unwrap(), "Hello, Sui");
}
+ #[test]
+ fn test_decode_utf8_cjk_rejected() {
+ // "你好,Sui" in UTF-8
+ let bytes = "e4bda0e5a5bdefbc8cs7569".replace('s', "");
+ let bytes = hex::decode(bytes + "2c20537569").unwrap();
+
+ let msg_str = decode_utf8(&bytes);
+ assert!(msg_str.is_err());
+ }
+
+ #[test]
+ fn test_decode_utf8_allows_emoji() {
+ // "Hello, 😀" in UTF-8, not CJK
+ let bytes = hex::decode("48656c6c6f2c20f09f9880").unwrap();
+ let msg_str = decode_utf8(&bytes);
+ assert_eq!(msg_str.unwrap(), "Hello, 😀");
+ }
+
+ #[test]
+ fn test_personal_message_bcs_roundtrip_and_parse() {
+ // Build a proper BCS-encoded IntentMessage<PersonalMessage>
+ let intent = types::intent::Intent::sui_app(IntentScope::PersonalMessage);
+ let original = IntentMessage::<PersonalMessage> {
+ intent,
+ value: PersonalMessage {
+ message: b"Hello via BCS".to_vec(),
+ },
+ };
+ let bytes = bcs::to_bytes(&original).unwrap();
+ // Ensure parse_intent recognizes it and preserves message
+ let parsed = parse_intent(&bytes).unwrap();
+ match parsed {
+ Intent::PersonalMessage(m) => {
+ assert_eq!(m.intent.app_id as u8, 0);
+ assert!(matches!(m.intent.scope, IntentScope::PersonalMessage));
+ assert_eq!(m.value.message, "Hello via BCS");
+ }
+ _ => panic!("unexpected variant"),
+ }
+ }
+
#[test]
fn test_sign() {
let seed = hex::decode("a33b2bdc2dc3c53d24081e5ed2273e6e8e0e43f8b26c746fbd1db2b8f1d4d8faa033545d3ec9303d36e743a4574b80b124353d380535532bb69455dc0ee442c4").unwrap();
@@ -227,7 +269,16 @@ mod tests {
#[test]
fn test_generate_address_wrong_length() {
- let pub_key = "edbe1b9b3b040ff88fbfa4ccda6f5f8d404ae7ffe35f9b220dec08679d5c336f";
+ // 62 hex chars -> 31 bytes (wrong length)
+ let pub_key = "edbe1b9b3b040ff88fbfa4ccda6f5f8d404ae7ffe35f9b220dec08679d5c33";
+ let result = generate_address(pub_key);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_generate_address_non_hex_prefix() {
+ // leading 0x should fail hex decoding
+ let pub_key = "0xedbe1b9b3b040ff88fbfa4ccda6f5f8d404ae7ffe35f9b220dec08679d5c336f";
let result = generate_address(pub_key);
assert!(result.is_err());
}
@@ -244,4 +295,92 @@ mod tests {
let result = parse_intent(&bytes);
assert!(result.is_err());
}
+
+ #[test]
+ fn test_parse_msg_too_short() {
+ // PersonalMessage scope (0x03) but total length < 4 should error in fallback path
+ let bytes = hex::decode("03").unwrap();
+ let result = parse_intent(&bytes);
+ assert!(result.is_err());
+ let bytes = hex::decode("0300").unwrap();
+ let result = parse_intent(&bytes);
+ assert!(result.is_err());
+ let bytes = hex::decode("030000").unwrap();
+ let result = parse_intent(&bytes);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_intent_constructors() {
+ let itx = types::intent::Intent::sui_transaction();
+ assert!(matches!(itx.scope, IntentScope::TransactionData));
+ assert_eq!(itx.version as u8, 0);
+ assert_eq!(itx.app_id as u8, 0);
+
+ let intent_pm = types::intent::Intent::sui_app(IntentScope::PersonalMessage);
+ assert!(matches!(intent_pm.scope, IntentScope::PersonalMessage));
+ assert_eq!(intent_pm.version as u8, 0);
+ assert_eq!(intent_pm.app_id as u8, 0);
+ }
+
+ #[test]
+ fn test_intent_scope_try_from_values() {
+ // Validate known discriminants 0..=6 map correctly
+ for (val, expect) in [
+ (0u8, IntentScope::TransactionData),
+ (1, IntentScope::TransactionEffects),
+ (2, IntentScope::CheckpointSummary),
+ (3, IntentScope::PersonalMessage),
+ (4, IntentScope::SenderSignedTransaction),
+ (5, IntentScope::ProofOfPossession),
+ (6, IntentScope::HeaderDigest),
+ ] {
+ let parsed = IntentScope::try_from(val).unwrap();
+ assert!(core::mem::discriminant(&parsed) == core::mem::discriminant(&expect));
+ }
+ // Out-of-range should fail
+ assert!(IntentScope::try_from(7u8).is_err());
+ assert!(IntentScope::try_from(255u8).is_err());
+ }
+
+ #[test]
+ fn test_intent_version_try_from_invalid() {
+ // Only V0(0) is valid
+ assert!(types::intent::IntentVersion::try_from(1u8).is_err());
+ assert!(types::intent::IntentVersion::try_from(255u8).is_err());
+ }
+
+ #[test]
+ fn test_intent_from_str_narwhal() {
+ // scope=TransactionData(0), version=V0(0), app=Narwhal(1) => 000001
+ let i = types::intent::Intent::from_str("000001").unwrap();
+ assert_eq!(i.scope as u8, 0);
+ assert_eq!(i.version as u8, 0);
+ assert_eq!(i.app_id as u8, 1);
+ }
+
+ #[test]
+ fn test_sign_intent_deterministic_and_path_isolation() {
+ let seed = hex::decode("a33b2bdc2dc3c53d24081e5ed2273e6e8e0e43f8b26c746fbd1db2b8f1d4d8faa033545d3ec9303d36e743a4574b80b124353d380535532bb69455dc0ee442c4").unwrap();
+ let path1 = "m/44'/784'/0'/0'/0'".to_string();
+ let path2 = "m/44'/784'/0'/0'/1'".to_string();
+ let msg = hex::decode("00000000000200201ff915a5e9e32fdbe0135535b6c69a00a9809aaf7f7c0275d3239ca79db20d6400081027000000000000020200010101000101020000010000ebe623e33b7307f1350f8934beb3fb16baef0fc1b3f1b92868eec3944093886901a2e3e42930675d9571a467eb5d4b22553c93ccb84e9097972e02c490b4e7a22ab73200000000000020176c4727433105da34209f04ac3f22e192a2573d7948cb2fabde7d13a7f4f149ebe623e33b7307f1350f8934beb3fb16baef0fc1b3f1b92868eec39440938869e803000000000000640000000000000000").unwrap();
+ let sig1a = sign_intent(&seed, &path1, &msg).unwrap();
+ let sig1b = sign_intent(&seed, &path1, &msg).unwrap();
+ assert_eq!(sig1a, sig1b); // deterministic
+ let sig2 = sign_intent(&seed, &path2, &msg).unwrap();
+ assert_ne!(sig1a, sig2); // different path => different signature
+ }
+
+ #[test]
+ fn test_utils_normalize_path_via_sui() {
+ // Ensure utils behavior is reachable and correct via sui crate dependency
+ let p = "44'/784'/0'".to_string();
+ let normalized = app_utils::normalize_path(&p);
+ assert_eq!(normalized, "m/44'/784'/0'");
+
+ let p2 = "m/44'/784'/0'".to_string();
+ let normalized2 = app_utils::normalize_path(&p2);
+ assert_eq!(normalized2, p2);
+ }
}
diff --git a/rust/apps/sui/src/types/intent.rs b/rust/apps/sui/src/types/intent.rs
index 1d67fc2..7989439 100644
--- a/rust/apps/sui/src/types/intent.rs
+++ b/rust/apps/sui/src/types/intent.rs
@@ -162,3 +162,43 @@ pub(crate) mod private {
pub trait SealedIntent {}
impl<T> SealedIntent for IntentMessage<T> {}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_intent_from_str_valid() {
+ // scope=TransactionData(0), version=V0(0), app=Sui(0) => 000000
+ let intent = Intent::from_str("000000").unwrap();
+ assert_eq!(intent.scope as u8, 0);
+ assert_eq!(intent.version as u8, 0);
+ assert_eq!(intent.app_id as u8, 0);
+ }
+
+ #[test]
+ fn test_intent_from_str_invalid_len() {
+ let res = Intent::from_str("00");
+ assert!(res.is_err());
+ }
+
+ #[test]
+ fn test_intent_from_str_invalid_scope() {
+ let res = Intent::from_str("ff0000");
+ assert!(res.is_err());
+ }
+
+ #[test]
+ fn test_intent_from_str_invalid_app_id() {
+ // scope=0, version=0, app_id=2 (invalid)
+ let res = Intent::from_str("000002");
+ assert!(res.is_err());
+ }
+
+ #[test]
+ fn test_intent_from_str_invalid_version() {
+ // scope=0, version=1 (invalid), app_id=0
+ let res = Intent::from_str("000100");
+ assert!(res.is_err());
+ }
+}
Why this scored 15/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.