What changed, and why it matters
This commit is a code-quality and hardening review of the Aptos blockchain support in the Keystone 3 hardware wallet firmware. It adds input validation (e.g., public keys must be exactly 32 bytes), removes risky simulator-only code paths that could dereference null pointers, fixes memory leaks and seed-handling bugs, and tightens how derivation paths and master fingerprints are extracted from signing requests. There is no explicit vendor statement that these changes fix a security vulnerability, but several of the corrected patterns are classic sources of bugs that could affect wallet safety.
Treat this as a defensive hardening patch. Reviewers should verify that the new length check covers all call paths for public-key input, that the C `free_ptr_string` and `memset_s` calls are present in the final binary, and that the removed simulator stubs did not mask any runtime behavior relied upon by tests. No immediate incident response is indicated, but the patch should be included in the next firmware release.
Security signals we found
Input validation added for public-key length in address derivation
Removal of simulator-only null-return and uninitialized-pointer code paths
Derivation-path and master-fingerprint extraction now handles missing entries instead of indexing first element unconditionally
Seed buffer now explicitly cleared after signing; seed length retrieval corrected
Memory leak fixed: `path` returned by `aptos_get_path` is now freed
Return value of `GetAccountSeed` is now checked before use
No explicit security advisory or CVE referenced in commit or supplied materials
Evidence from the diff
The patch refactors Aptos signing and parsing code across Rust and C layers. Key changes: (1) generate_address now rejects non-32-byte public keys instead of silently hashing arbitrary input. (2) parse_tx/parse_msg accept slices instead of owned Vecs, reducing allocations and copy-paste errors. (3) is_tx uses starts_with on a const prefix. (4) Rust-C FFI functions no longer assume the first derivation path exists; they search for a valid path/fingerprint and return explicit errors. (5) The C simulator stubs that returned NULL or dereferenced an uninitialized UREncodeResult are removed, so the same code runs in simulator and device builds. (6) GuiGetAptosSignQrCodeData now checks GetAccountSeed return value, frees the returned path string, clears the seed buffer with memset_s, and uses GetCurrentAccountSeedLen instead of a conditional entropy length. (7) New unit tests cover address generation and message parsing behavior.
Changed components
rust/apps/aptos/src/lib.rsrust/apps/aptos/src/parser.rsrust/rust_c/src/aptos/mod.rssrc/ui/gui_chain/multi/web3/gui_aptos.cInspect captured patch +93 / −75
diff --git a/rust/apps/aptos/src/lib.rs b/rust/apps/aptos/src/lib.rs
index eb37d8e..8e47cd4 100644
--- a/rust/apps/aptos/src/lib.rs
+++ b/rust/apps/aptos/src/lib.rs
@@ -25,16 +25,21 @@ pub mod parser;
pub fn generate_address(pub_key: &str) -> Result<String> {
let mut buf: Vec<u8> = hex::decode(pub_key)?;
+ if buf.len() != 32 {
+ return Err(errors::AptosError::InvalidData(
+ "public key must be 32 bytes".to_string(),
+ ));
+ }
buf.push(0);
let addr = Sha3_256::new().update(&buf).finalize();
Ok(format!("0x{}", hex::encode(addr)))
}
-pub fn parse_tx(data: &Vec<u8>) -> crate::errors::Result<AptosTx> {
+pub fn parse_tx(data: &[u8]) -> Result<AptosTx> {
Parser::parse_tx(data)
}
-pub fn parse_msg(data: &Vec<u8>) -> crate::errors::Result<String> {
+pub fn parse_msg(data: &[u8]) -> Result<String> {
Parser::parse_msg(data)
}
@@ -46,9 +51,7 @@ pub fn sign(message: Vec<u8>, hd_path: &String, seed: &[u8]) -> errors::Result<[
#[cfg(test)]
mod tests {
extern crate std;
-
use super::*;
-
use hex::FromHex;
use hex::ToHex;
@@ -69,4 +72,38 @@ mod tests {
let signature = sign(tx_hex, &hd_path, seed.as_slice()).unwrap();
assert_eq!("ff2c5e05557c30d1cddd505b26836747eaf28f25b2816b1e702bd40236be674eaaef10e4bd940b85317bede537cad22365eb7afca7456b90dcc2807cbbdcaa0a", signature.encode_hex::<String>());
}
+
+ #[test]
+ fn test_generate_address_ok() {
+ // 32-byte pubkey all zeros
+ let pubkey_hex = "0000000000000000000000000000000000000000000000000000000000000000";
+ let addr = generate_address(pubkey_hex).unwrap();
+
+ // compute expected = sha3_256(pubkey || 0x00)
+ let mut buf = Vec::from_hex(pubkey_hex).unwrap();
+ buf.push(0);
+ let expected = Sha3_256::new().update(&buf).finalize();
+ let expected_addr = format!("0x{}", hex::encode(expected));
+
+ assert_eq!(addr, expected_addr);
+ }
+
+ #[test]
+ fn test_generate_address_invalid_hex() {
+ let res = generate_address("zz");
+ assert!(res.is_err());
+ }
+
+ #[test]
+ fn test_parse_msg_ascii_and_cjk() {
+ // ASCII returns utf8 directly
+ let ascii = b"hello, aptos".to_vec();
+ let ascii_out = parse_msg(&ascii).unwrap();
+ assert_eq!(ascii_out, "hello, aptos");
+
+ // CJK should be hex-encoded output according to parser policy
+ let cjk = "中文".as_bytes().to_vec();
+ let cjk_out = parse_msg(&cjk).unwrap();
+ assert_eq!(cjk_out, hex::encode(&cjk));
+ }
}
diff --git a/rust/apps/aptos/src/parser.rs b/rust/apps/aptos/src/parser.rs
index efc764f..1ddccf4 100644
--- a/rust/apps/aptos/src/parser.rs
+++ b/rust/apps/aptos/src/parser.rs
@@ -2,7 +2,6 @@ use crate::aptos_type::RawTransaction;
use crate::errors::{self, AptosError, Result};
use alloc::format;
use alloc::string::{String, ToString};
-use alloc::vec::Vec;
use bcs;
use hex;
@@ -11,41 +10,38 @@ use serde_json::{json, Value};
pub struct Parser;
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) {
+ match core::str::from_utf8(msg) {
+ Ok(s) => {
+ if app_utils::is_cjk(s) {
Err(errors::AptosError::InvalidData(String::from(
"contains CJK",
)))
} else {
- Ok(utf8_msg)
+ Ok(s.to_string())
}
}
Err(e) => Err(errors::AptosError::InvalidData(e.to_string())),
}
}
-pub fn is_tx(data: &Vec<u8>) -> bool {
+pub fn is_tx(data: &[u8]) -> bool {
// prefix bytes is sha3_256("APTOS::RawTransaction")
- let tx_prefix = [
+ const TX_PREFIX: [u8; 32] = [
0xb5, 0xe9, 0x7d, 0xb0, 0x7f, 0xa0, 0xbd, 0x0e, 0x55, 0x98, 0xaa, 0x36, 0x43, 0xa9, 0xbc,
0x6f, 0x66, 0x93, 0xbd, 0xdc, 0x1a, 0x9f, 0xec, 0x9e, 0x67, 0x4a, 0x46, 0x1e, 0xaa, 0x00,
0xb1, 0x93,
];
- data.len() > 32 && data[..32] == tx_prefix
+ data.len() > 32 && data.starts_with(&TX_PREFIX)
}
impl Parser {
- pub fn parse_tx(data: &Vec<u8>) -> Result<AptosTx> {
- let mut data_parse = data.clone();
- if is_tx(data) {
- data_parse = data[32..].to_vec();
- }
- let tx: RawTransaction = bcs::from_bytes(&data_parse)
+ pub fn parse_tx(data: &[u8]) -> Result<AptosTx> {
+ let data_slice = if is_tx(data) { &data[32..] } else { data };
+ let tx: RawTransaction = bcs::from_bytes(data_slice)
.map_err(|err| AptosError::ParseTxError(format!("bcs deserialize failed {err}")))?;
Ok(AptosTx::new(tx))
}
- pub fn parse_msg(data: &Vec<u8>) -> Result<String> {
+ pub fn parse_msg(data: &[u8]) -> Result<String> {
match decode_utf8(data) {
Ok(v) => Ok(v),
Err(_) => Ok(hex::encode(data)),
diff --git a/rust/rust_c/src/aptos/mod.rs b/rust/rust_c/src/aptos/mod.rs
index 0869e53..f3de2f3 100644
--- a/rust/rust_c/src/aptos/mod.rs
+++ b/rust/rust_c/src/aptos/mod.rs
@@ -28,20 +28,23 @@ unsafe fn build_sign_result(
) -> app_aptos::errors::Result<AptosSignature> {
let sign_request = extract_ptr_with_type!(ptr, AptosSignRequest);
let pub_key = recover_c_char(pub_key);
- let mut path = sign_request.get_authentication_key_derivation_paths()[0]
- .get_path()
- .ok_or(AptosError::InvalidData(
- "invalid derivation path".to_string(),
- ))?;
+ let paths = sign_request.get_authentication_key_derivation_paths();
+ let mut path = match paths.iter().find_map(|dp| dp.get_path()) {
+ Some(p) => p,
+ None => {
+ return Err(AptosError::InvalidData(
+ "empty or missing derivation path".to_string(),
+ ))
+ }
+ };
if !path.starts_with("m/") {
path = format!("m/{path}");
}
let signature = app_aptos::sign(sign_request.get_sign_data().to_vec(), &path, seed)?;
- let buf: Vec<u8> = hex::decode(pub_key)?;
Ok(AptosSignature::new(
sign_request.get_request_id(),
signature.to_vec(),
- buf,
+ hex::decode(pub_key)?,
))
}
@@ -66,17 +69,21 @@ pub unsafe extern "C" fn aptos_check_request(
}
let mfp = extract_array!(master_fingerprint, u8, 4);
let sign_request = extract_ptr_with_type!(ptr, AptosSignRequest);
- let ur_mfp = sign_request.get_authentication_key_derivation_paths()[0].get_source_fingerprint();
+ let ur_mfp = match sign_request
+ .get_authentication_key_derivation_paths()
+ .iter()
+ .find_map(|dp| dp.get_source_fingerprint())
+ {
+ Some(mfp) => mfp,
+ None => return TransactionCheckResult::from(RustCError::InvalidHDPath).c_ptr(),
+ };
if let Ok(mfp) = mfp.try_into() as Result<[u8; 4], _> {
- if let Some(ur_mfp) = ur_mfp {
- return if mfp == ur_mfp {
- TransactionCheckResult::new().c_ptr()
- } else {
- TransactionCheckResult::from(RustCError::MasterFingerprintMismatch).c_ptr()
- };
- }
- TransactionCheckResult::from(RustCError::MasterFingerprintMismatch).c_ptr()
+ return if mfp == ur_mfp {
+ TransactionCheckResult::new().c_ptr()
+ } else {
+ TransactionCheckResult::from(RustCError::MasterFingerprintMismatch).c_ptr()
+ };
} else {
TransactionCheckResult::from(RustCError::InvalidMasterFingerprint).c_ptr()
}
@@ -84,21 +91,14 @@ pub unsafe extern "C" fn aptos_check_request(
#[no_mangle]
pub unsafe extern "C" fn aptos_parse(ptr: PtrUR) -> PtrT<TransactionParseResult<DisplayAptosTx>> {
- let sign_request = extract_ptr_with_type!(ptr, AptosSignRequest);
+ let sign_request: &mut AptosSignRequest = extract_ptr_with_type!(ptr, AptosSignRequest);
let sign_data = sign_request.get_sign_data();
let sign_type = match sign_request.get_sign_type() {
- SignType::Single => {
- if is_tx(&sign_data) {
- SignType::Single
- } else {
- SignType::Message
- }
- }
- SignType::Multi => SignType::Multi,
- SignType::Message => SignType::Message,
+ SignType::Single if !is_tx(sign_data.as_slice()) => SignType::Message,
+ other => other,
};
match sign_type {
- SignType::Single => match app_aptos::parse_tx(&sign_data.to_vec()) {
+ SignType::Single => match app_aptos::parse_tx(sign_data.as_slice()) {
Ok(v) => TransactionParseResult::success(DisplayAptosTx::from(v).c_ptr()).c_ptr(),
Err(e) => TransactionParseResult::from(e).c_ptr(),
},
@@ -106,7 +106,7 @@ pub unsafe extern "C" fn aptos_parse(ptr: PtrUR) -> PtrT<TransactionParseResult<
TransactionParseResult::from(AptosError::ParseTxError("not support".to_string()))
.c_ptr()
}
- SignType::Message => match app_aptos::parse_msg(&sign_data.to_vec()) {
+ SignType::Message => match app_aptos::parse_msg(sign_data.as_slice()) {
Ok(v) => TransactionParseResult::success(DisplayAptosTx::from(v).c_ptr()).c_ptr(),
Err(e) => TransactionParseResult::from(e).c_ptr(),
},
diff --git a/src/ui/gui_chain/multi/web3/gui_aptos.c b/src/ui/gui_chain/multi/web3/gui_aptos.c
index a9d73cb..ae112e6 100644
--- a/src/ui/gui_chain/multi/web3/gui_aptos.c
+++ b/src/ui/gui_chain/multi/web3/gui_aptos.c
@@ -34,7 +34,6 @@ void GuiSetAptosUrData(URParseResult *urResult, URParseMultiResult *urMultiResul
void *GuiGetAptosData(void)
{
-#ifndef COMPILE_SIMULATOR
CHECK_FREE_PARSE_RESULT(g_parseResult);
uint8_t mfp[4];
void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
@@ -45,30 +44,21 @@ void *GuiGetAptosData(void)
g_parseResult = (void *)parseResult;
} while (0);
return g_parseResult;
-#else
- return NULL;
-#endif
}
PtrT_TransactionCheckResult GuiGetAptosCheckResult(void)
{
-#ifndef COMPILE_SIMULATOR
uint8_t mfp[4];
void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
GetMasterFingerPrint(mfp);
return aptos_check_request(data, mfp, sizeof(mfp));
-#else
- return NULL;
-#endif
}
void FreeAptosMemory(void)
{
-#ifndef COMPILE_SIMULATOR
CHECK_FREE_UR_RESULT(g_urResult, false);
CHECK_FREE_UR_RESULT(g_urMultiResult, true);
CHECK_FREE_PARSE_RESULT(g_parseResult);
-#endif
}
int GetAptosDetailLen(void *param)
@@ -96,31 +86,26 @@ UREncodeResult *GuiGetAptosSignQrCodeData(void)
{
bool enable = IsPreviousLockScreenEnable();
SetLockScreen(false);
-#ifndef COMPILE_SIMULATOR
- UREncodeResult *encodeResult;
+ UREncodeResult *encodeResult = NULL;
void *data = g_isMulti ? g_urMultiResult->data : g_urResult->data;
+ uint8_t seed[SEED_LEN];
+ int ret = 0;
+
do {
- uint8_t seed[64];
- GetAccountSeed(GetCurrentAccountIndex(), seed, SecretCacheGetPassword());
+ ret = GetAccountSeed(GetCurrentAccountIndex(), seed, SecretCacheGetPassword());
+ if (ret != SUCCESS_CODE) {
+ break;
+ }
char *path = aptos_get_path(data);
- char pubkeyIndex = GetAptosPublickeyIndex(path);
- char *pubKey = GetCurrentAccountPublicKey(pubkeyIndex);
- int len = GetMnemonicType() == MNEMONIC_TYPE_BIP39 ? sizeof(seed) : GetCurrentAccountEntropyLen();
- encodeResult = aptos_sign_tx(data, seed, len, pubKey);
- ClearSecretCache();
+ char *pubKey = GetCurrentAccountPublicKey(GetAptosPublickeyIndex(path));
+ free_ptr_string(path);
+ encodeResult = aptos_sign_tx(data, seed, GetCurrentAccountSeedLen(), pubKey);
CHECK_CHAIN_BREAK(encodeResult);
} while (0);
+ memset_s(seed, sizeof(seed), 0, sizeof(seed));
+ ClearSecretCache();
SetLockScreen(enable);
return encodeResult;
-#else
- UREncodeResult *encodeResult = NULL;
- encodeResult->is_multi_part = 0;
- encodeResult->data = "xpub6CZZYZBJ857yVCZXzqMBwuFMogBoDkrWzhsFiUd1SF7RUGaGryBRtpqJU6AGuYGpyabpnKf5SSMeSw9E9DSA8ZLov53FDnofx9wZLCpLNft";
- encodeResult->encoder = NULL;
- encodeResult->error_code = 0;
- encodeResult->error_message = NULL;
- return encodeResult;
-#endif
}
static uint8_t GetAptosPublickeyIndex(char* rootPath)
Why this scored 34/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.