What changed, and why it matters
This commit only adds new automated test cases for the XRP app in the Keystone 3 firmware. It does not change any production code, fix bugs, or alter behavior. The tests exercise functions like address derivation, transaction parsing, signing, and public-key path lookup with valid and invalid inputs. There is no security patch or vulnerability present in the diff itself.
No security action required. Treat as routine test-coverage improvement. If reviewing for security, use these tests as a baseline to verify that the underlying XRP functions already handle the tested error cases correctly in production builds.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit adds 275 lines across two Rust test modules: rust/apps/xrp/src/address/mod.rs and rust/apps/xrp/src/lib.rs. It introduces unit tests for get_address, check_tx, parse, sign_tx, get_tx_hash, and get_pubkey_path. The tests cover error paths such as invalid HD paths, malformed xpubs, invalid JSON, out-of-range destination tags, and missing public keys. The only production-code change is a comment added in check_tx noting the 200-level derivation limit. No runtime logic is modified.
Changed components
rust/apps/xrp/src/address/mod.rsrust/apps/xrp/src/lib.rsInspect captured patch +275 / −0
diff --git a/rust/apps/xrp/src/address/mod.rs b/rust/apps/xrp/src/address/mod.rs
index 7dc4f92..49702ee 100644
--- a/rust/apps/xrp/src/address/mod.rs
+++ b/rust/apps/xrp/src/address/mod.rs
@@ -25,6 +25,7 @@ mod tests {
extern crate std;
use crate::address::get_address;
+ use crate::errors::XRPError;
use alloc::string::ToString;
#[test]
@@ -46,5 +47,120 @@ mod tests {
let address = get_address(path, extended_pub_key, root_path).unwrap();
assert_eq!("r4Sh61HP7nxB6mQxXSSeN2DCkG3sTrzb2c".to_string(), address);
}
+
+ }
+
+ #[test]
+ fn test_invalid_hd_path_prefix() {
+ let extended_pub_key = "xpub6CFKyZTfzj3cyeRLUDKwQQ5s1tqTTdVgywKMVkrB2i1taGFbhazkxDzWVsfBHZpv7rg6qpDBGYR5oA8iazEfa44CdQkkknPFHJ7YCzncCS9";
+ let root_path = "44'/144'/0'";
+
+ // HD path doesn't start with root path
+ let path = "44'/144'/1'/0/0";
+ let result = get_address(path, extended_pub_key, root_path);
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), XRPError::InvalidHDPath(_)));
+ }
+
+ #[test]
+ fn test_completely_different_path() {
+ let extended_pub_key = "xpub6CFKyZTfzj3cyeRLUDKwQQ5s1tqTTdVgywKMVkrB2i1taGFbhazkxDzWVsfBHZpv7rg6qpDBGYR5oA8iazEfa44CdQkkknPFHJ7YCzncCS9";
+ let root_path = "44'/144'/0'";
+
+ // Completely different path
+ let path = "44'/60'/0'/0/0";
+ let result = get_address(path, extended_pub_key, root_path);
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), XRPError::InvalidHDPath(_)));
+ }
+
+ #[test]
+ fn test_empty_hd_path() {
+ let extended_pub_key = "xpub6CFKyZTfzj3cyeRLUDKwQQ5s1tqTTdVgywKMVkrB2i1taGFbhazkxDzWVsfBHZpv7rg6qpDBGYR5oA8iazEfa44CdQkkknPFHJ7YCzncCS9";
+ let root_path = "44'/144'/0'";
+
+ // Empty HD path
+ let path = "";
+ let result = get_address(path, extended_pub_key, root_path);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_empty_root_path() {
+ let extended_pub_key = "xpub6CFKyZTfzj3cyeRLUDKwQQ5s1tqTTdVgywKMVkrB2i1taGFbhazkxDzWVsfBHZpv7rg6qpDBGYR5oA8iazEfa44CdQkkknPFHJ7YCzncCS9";
+
+ // Empty root path should still work if path is also appropriate
+ let root_path = "";
+ let path = "0/0";
+ let result = get_address(path, extended_pub_key, root_path);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_invalid_extended_pub_key() {
+ let root_path = "44'/144'/0'";
+ let path = "44'/144'/0'/0/0";
+
+ // Invalid extended public key format
+ let invalid_xpub = "invalid_xpub_key";
+ let result = get_address(path, invalid_xpub, root_path);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_malformed_extended_pub_key() {
+ let root_path = "44'/144'/0'";
+ let path = "44'/144'/0'/0/0";
+
+ // Malformed extended public key (too short)
+ let invalid_xpub = "xpub";
+ let result = get_address(path, invalid_xpub, root_path);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_path_shorter_than_root() {
+ let extended_pub_key = "xpub6CFKyZTfzj3cyeRLUDKwQQ5s1tqTTdVgywKMVkrB2i1taGFbhazkxDzWVsfBHZpv7rg6qpDBGYR5oA8iazEfa44CdQkkknPFHJ7YCzncCS9";
+ let root_path = "44'/144'/0'/0/0";
+
+ // Path is shorter than root path
+ let path = "44'/144'/0'";
+ let result = get_address(path, extended_pub_key, root_path);
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), XRPError::InvalidHDPath(_)));
+ }
+
+ #[test]
+ fn test_malformed_hd_path_format() {
+ let extended_pub_key = "xpub6CFKyZTfzj3cyeRLUDKwQQ5s1tqTTdVgywKMVkrB2i1taGFbhazkxDzWVsfBHZpv7rg6qpDBGYR5oA8iazEfa44CdQkkknPFHJ7YCzncCS9";
+ let root_path = "44'/144'/0'";
+
+ // Malformed HD path (invalid characters)
+ let path = "44'/144'/0'/0/abc";
+ let result = get_address(path, extended_pub_key, root_path);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_root_path_with_trailing_slash() {
+ let extended_pub_key = "xpub6CFKyZTfzj3cyeRLUDKwQQ5s1tqTTdVgywKMVkrB2i1taGFbhazkxDzWVsfBHZpv7rg6qpDBGYR5oA8iazEfa44CdQkkknPFHJ7YCzncCS9";
+
+ // Root path already has trailing slash - should still work
+ let root_path = "44'/144'/0'/";
+ let path = "44'/144'/0'/0/0";
+ let result = get_address(path, extended_pub_key, root_path);
+ assert!(result.is_ok());
+ assert_eq!("rHsMGQEkVNJmpGWs8XUBoTBiAAbwxZN5v3".to_string(), result.unwrap());
+ }
+
+ #[test]
+ fn test_special_characters_in_path() {
+ let extended_pub_key = "xpub6CFKyZTfzj3cyeRLUDKwQQ5s1tqTTdVgywKMVkrB2i1taGFbhazkxDzWVsfBHZpv7rg6qpDBGYR5oA8iazEfa44CdQkkknPFHJ7YCzncCS9";
+ let root_path = "44'/144'/0'";
+
+ // Path with special characters
+ let path = "44'/144'/0'/@#$/!@#";
+ let result = get_address(path, extended_pub_key, root_path);
+ assert!(result.is_err());
}
}
diff --git a/rust/apps/xrp/src/lib.rs b/rust/apps/xrp/src/lib.rs
index a7ed5f9..a629281 100644
--- a/rust/apps/xrp/src/lib.rs
+++ b/rust/apps/xrp/src/lib.rs
@@ -73,6 +73,7 @@ pub fn check_tx(raw: &[u8], root_xpub: &str, cached_pubkey: &str) -> R<String> {
if wrapped_tx.signing_pubkey == cached_pubkey {
return Ok("".to_string());
}
+ // only support 200 levels of derivation
get_pubkey_path(root_xpub, &wrapped_tx.signing_pubkey, 200)
}
@@ -112,4 +113,162 @@ mod tests {
let result = check_tx(&raw, root_xpub, "");
assert!(result.is_ok());
}
+
+ #[test]
+ fn test_check_tx_with_cached_pubkey() {
+ let raw = hex::decode("7B225472616E73616374696F6E54797065223A225061796D656E74222C22416D6F756E74223A223130303030303030222C2244657374696E6174696F6E223A22724A6436416D48485A7250756852683377536637696B724D4A516639373646516462222C22466C616773223A323134373438333634382C224163636F756E74223A227247556D6B794C627671474633687758347177474864727A4C6459325170736B756D222C22466565223A223132222C2253657175656E6365223A34323532393130372C224C6173744C656467657253657175656E6365223A34323532393137332C225369676E696E675075624B6579223A22303346354335424231443139454337313044334437464144313939414631304346384243314431313334384535423337363543304230423943304245433332383739227D").unwrap();
+ let root_xpub = "xpub6Czwh4mKUQryD6dbe9e9299Gjn4EnSP641rACmQeAhCXYjW4Hnj8tqiCMir3VSWoNnjimtzy6qtnjD1GSf8FtEdXUFcGeXXezXyEMXtMmo1";
+ let cached_pubkey = "03F5C5BB1D19EC710D3D7FAD199AF10CF8BC1D11348E5B3765C0B0B9C0BEC32879";
+ let result = check_tx(&raw, root_xpub, cached_pubkey);
+ assert!(result.is_ok());
+ assert_eq!(result.unwrap(), "");
+ }
+
+ #[test]
+ fn test_check_tx_invalid_pubkey() {
+ let raw = hex::decode("7B225472616E73616374696F6E54797065223A225061796D656E74222C22416D6F756E74223A223130303030303030222C2244657374696E6174696F6E223A22724A6436416D48485A7250756852683377536637696B724D4A516639373646516462222C22466C616773223A323134373438333634382C224163636F756E74223A227247556D6B794C627671474633687758347177474864727A4C6459325170736B756D222C22466565223A223132222C2253657175656E6365223A34323532393130372C224C6173744C656467657253657175656E6365223A34323532393137332C225369676E696E675075624B6579223A22303146354335424231443139454337313044334437464144313939414631304346384243314431313334384535423337363543304230423943304245433332383739227D").unwrap();
+ let root_xpub = "xpub6Czwh4mKUQryD6dbe9e9299Gjn4EnSP641rACmQeAhCXYjW4Hnj8tqiCMir3VSWoNnjimtzy6qtnjD1GSf8FtEdXUFcGeXXezXyEMXtMmo1";
+ let result = check_tx(&raw, root_xpub, "");
+ assert!(result.is_err());
+ }
+
+
+ #[test]
+ fn test_get_tx_hash_invalid_json() {
+ let raw_hex = hex::encode(b"invalid json");
+ let result = get_tx_hash(hex::decode(raw_hex).unwrap().as_slice());
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_parse_payment_tx() {
+ let raw_hex = "7B225472616E73616374696F6E54797065223A225061796D656E74222C22416D6F756E74223A223130303030303030222C2244657374696E6174696F6E223A22724A6436416D48485A7250756852683377536637696B724D4A516639373646516462222C22466C616773223A323134373438333634382C224163636F756E74223A227247556D6B794C627671474633687758347177474864727A4C6459325170736B756D222C22466565223A223132222C2253657175656E6365223A34323532393130372C224C6173744C656467657253657175656E6365223A34323532393137332C225369676E696E675075624B6579223A22303346354335424231443139454337313044334437464144313939414631304346384243314431313334384535423337363543304230423943304245433332383739227D";
+ let parsed = parse(hex::decode(raw_hex).unwrap().as_slice()).unwrap();
+ assert_eq!(parsed.network, "XRP Mainnet");
+ assert_eq!(parsed.display_type, parser::structs::XrpTxDisplayType::Payment);
+ assert_eq!(
+ parsed.signing_pubkey,
+ "03F5C5BB1D19EC710D3D7FAD199AF10CF8BC1D11348E5B3765C0B0B9C0BEC32879"
+ );
+ }
+
+ #[test]
+ fn test_parse_invalid_json() {
+ let raw_hex = hex::encode(b"not a valid json");
+ let result = parse(hex::decode(raw_hex).unwrap().as_slice());
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_parse_missing_transaction_type() {
+ let raw_hex = hex::encode(b"{\"Account\":\"test\"}");
+ let result = parse(hex::decode(raw_hex).unwrap().as_slice());
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_sign_tx_invalid_json() {
+ let hd_path = String::from("m/44'/144'/0'/0/0");
+ let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
+ let raw_hex = hex::encode(b"invalid json");
+ let result = sign_tx(
+ hex::decode(raw_hex).unwrap().as_slice(),
+ &hd_path,
+ seed.as_slice(),
+ );
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_sign_tx_with_invalid_destination_tag() {
+ let hd_path = String::from("m/44'/144'/0'/0/0");
+ let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
+ // Destination tag exceeds u32::MAX
+ let raw_hex = "7B2253657175656E6365223A312C22466565223A223230222C224163636F756E74223A22724C354259534C643839757A6A3469344A3437694C51673948776D65584537654374222C2244657374696E6174696F6E223A227248666F6631784E6245744A5973584E384D55626E66396946697843455938346B66222C2244657374696E6174696F6E546167223A343239343936373239362C22416D6F756E74223A2231303030303030222C225472616E73616374696F6E54797065223A225061796D656E74222C22466C616773223A323134373438333634382C225369676E696E675075624B6579223A22303331443638424331413134324536373636423242444642303036434346453133354546324530453245393441424235434635433941423631303437373646424145227D";
+ let result = sign_tx(
+ hex::decode(raw_hex).unwrap().as_slice(),
+ &hd_path,
+ seed.as_slice(),
+ );
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), XRPError::SignFailure(_)));
+ }
+
+ #[test]
+ fn test_get_pubkey_path_not_found() {
+ let pubkey = "02AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
+ let root_xpub = "xpub6Czwh4mKUQryD6dbe9e9299Gjn4EnSP641rACmQeAhCXYjW4Hnj8tqiCMir3VSWoNnjimtzy6qtnjD1GSf8FtEdXUFcGeXXezXyEMXtMmo1";
+ let result = get_pubkey_path(root_xpub, pubkey, 10);
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), XRPError::InvalidData(_)));
+ }
+
+ #[test]
+ fn test_get_pubkey_path_invalid_xpub() {
+ let pubkey = "03F5C5BB1D19EC710D3D7FAD199AF10CF8BC1D11348E5B3765C0B0B9C0BEC32879";
+ let root_xpub = "invalid_xpub";
+ let result = get_pubkey_path(root_xpub, pubkey, 200);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_get_pubkey_path_invalid_hex() {
+ let pubkey = "not_valid_hex";
+ let root_xpub = "xpub6Czwh4mKUQryD6dbe9e9299Gjn4EnSP641rACmQeAhCXYjW4Hnj8tqiCMir3VSWoNnjimtzy6qtnjD1GSf8FtEdXUFcGeXXezXyEMXtMmo1";
+ let result = get_pubkey_path(root_xpub, pubkey, 200);
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_xrp_sign_different_path() {
+ let hd_path = String::from("m/44'/144'/0'/0/1");
+ let raw_hex = "7B2253657175656E6365223A312C22466565223A223230222C224163636F756E74223A22724C354259534C643839757A6A3469344A3437694C51673948776D65584537654374222C2244657374696E6174696F6E223A227248666F6631784E6245744A5973584E384D55626E66396946697843455938346B66222C2244657374696E6174696F6E546167223A313730303337333336342C22416D6F756E74223A2231303030303030222C225472616E73616374696F6E54797065223A225061796D656E74222C22466C616773223A323134373438333634382C225369676E696E675075624B6579223A22303331443638424331413134324536373636423242444642303036434346453133354546324530453245393441424235434635433941423631303437373646424145227D";
+ let seed = hex::decode("5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4").unwrap();
+ let result = sign_tx(
+ hex::decode(raw_hex).unwrap().as_slice(),
+ &hd_path,
+ seed.as_slice(),
+ );
+ assert!(result.is_ok());
+ let signed_tx = result.unwrap();
+ // Verify it's valid hex and not empty
+ assert!(!signed_tx.is_empty());
+ }
+
+ #[test]
+ fn test_parse_usd_payment() {
+ let tx_str = r#"{
+ "TransactionType" : "Payment",
+ "Account" : "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn",
+ "Destination" : "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX",
+ "Amount" : {
+ "currency" : "USD",
+ "value" : "1",
+ "issuer" : "rf1BiGeXwwQoi8Z2ueFYTEXSwuJYfV2Jpn"
+ },
+ "Fee": "12",
+ "Flags": 2147483648,
+ "Sequence": 2,
+ "SigningPubKey": "03F5C5BB1D19EC710D3D7FAD199AF10CF8BC1D11348E5B3765C0B0B9C0BEC32879"
+ }"#;
+ let parsed = parse(tx_str.as_bytes()).unwrap();
+ assert_eq!(parsed.display_type, parser::structs::XrpTxDisplayType::Payment);
+ }
+
+ #[test]
+ fn test_check_tx_invalid_json() {
+ let raw = hex::encode(b"invalid json");
+ let root_xpub = "xpub6Czwh4mKUQryD6dbe9e9299Gjn4EnSP641rACmQeAhCXYjW4Hnj8tqiCMir3VSWoNnjimtzy6qtnjD1GSf8FtEdXUFcGeXXezXyEMXtMmo1";
+ let result = check_tx(hex::decode(raw).unwrap().as_slice(), root_xpub, "");
+ assert!(result.is_err());
+ }
+
+ #[test]
+ fn test_get_pubkey_path_with_zero_max() {
+ let pubkey = "03F5C5BB1D19EC710D3D7FAD199AF10CF8BC1D11348E5B3765C0B0B9C0BEC32879";
+ let root_xpub = "xpub6Czwh4mKUQryD6dbe9e9299Gjn4EnSP641rACmQeAhCXYjW4Hnj8tqiCMir3VSWoNnjimtzy6qtnjD1GSf8FtEdXUFcGeXXezXyEMXtMmo1";
+ let result = get_pubkey_path(root_xpub, pubkey, 0);
+ assert!(result.is_err());
+ assert!(matches!(result.unwrap_err(), XRPError::InvalidData(_)));
+ }
}
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.