What changed, and why it matters
This firmware update adds a safety check that refuses to use cryptocurrency seeds made entirely of 0x00 or 0xFF bytes across Cardano, Bitcoin-style (secp256k1), Ed25519, RSA, and Zcash key operations. Such 'trivial' seeds produce predictable or weak private keys that attackers could guess. The patch also moves a memory-clearing call in Ethereum batch signing so the seed is wiped only after all transactions are processed, rather than after the first one. The commit message does not call this a security fix, but the change clearly reduces a key-derivation risk.
Treat as a hardening/security-improvement commit. Review whether the new `is_all_zero_or_ff` check is enforced at every seed entry point, including backup/restore and BIP39/SLIP39 generation paths. Verify that the moved `seed.zeroize()` in Ethereum batch signing does not leave seed material accessible longer than intended and that error paths still clear secrets. Consider adding tests for all-zero/all-0xFF seed rejection and for batch signing with multiple transactions.
Security signals we found
Rejection of all-zero and all-0xFF seeds in multiple key-derivation paths
Prevention of deterministic weak master keys for secp256k1, Ed25519/SLIP-10, Cardano/SLIP-23, RSA, and Zcash
Fixes premature zeroization of seed inside Ethereum batch signing loop, ensuring consistent seed availability across batch items
Uses zeroize crate for secret clearing
No CVE, advisory, or vendor security description present in supplied materials
Evidence from the diff
The commit introduces a shared helper is_all_zero_or_ff and calls it before seed-dependent operations in slip23 (Cardano), slip10_ed25519, secp256k1, RSA, and Zcash modules. It rejects seeds whose every byte is 0x00 or 0xFF. In rust/rust_c/src/ethereum/mod.rs, seed.zeroize() is moved outside the transaction loop so the seed remains available for every signature in a batch and is zeroized once at the end. The patch is defensive and partial: it does not document whether other all-identical byte patterns or very low-entropy seeds are also considered invalid, and it does not show where the seed originates or how an attacker could force a trivial seed.
Changed components
rust/apps/cardano/src/slip23.rsrust/keystore/src/algorithms/ed25519/slip10_ed25519.rsrust/keystore/src/algorithms/rsa/mod.rsrust/keystore/src/algorithms/secp256k1.rsrust/keystore/src/algorithms/utils.rsrust/keystore/src/algorithms/zcash/mod.rsrust/rust_c/src/ethereum/mod.rsInspect captured patch +53 / −5
diff --git a/rust/apps/cardano/src/slip23.rs b/rust/apps/cardano/src/slip23.rs
index 87d5711..73699dd 100644
--- a/rust/apps/cardano/src/slip23.rs
+++ b/rust/apps/cardano/src/slip23.rs
@@ -6,8 +6,8 @@ use keystore::algorithms::crypto::hmac_sha512;
// https://github.com/satoshilabs/slips/blob/master/slip-0023.md
pub fn from_seed_slip23(seed: &[u8]) -> Result<XPrv> {
- if seed.is_empty() {
- return Err(CardanoError::InvalidSeed("seed is empty".to_string()));
+ if seed.is_empty() || seed.iter().all(|b| *b == 0x00) || seed.iter().all(|b| *b == 0xFF) {
+ return Err(CardanoError::InvalidSeed("seed is invalid".to_string()));
}
// Step 2: Calculate I := HMAC-SHA512(Key = "ed25519 cardano seed", Data = S)
diff --git a/rust/keystore/src/algorithms/ed25519/slip10_ed25519.rs b/rust/keystore/src/algorithms/ed25519/slip10_ed25519.rs
index 5edfc73..b997f23 100644
--- a/rust/keystore/src/algorithms/ed25519/slip10_ed25519.rs
+++ b/rust/keystore/src/algorithms/ed25519/slip10_ed25519.rs
@@ -6,9 +6,16 @@ use bitcoin::bip32::{ChildNumber, DerivationPath};
use zeroize::Zeroize;
use crate::algorithms::crypto::hmac_sha512;
-use crate::algorithms::utils::normalize_path;
+use crate::algorithms::utils::{is_all_zero_or_ff, normalize_path};
use crate::errors::{KeystoreError, Result};
+fn ensure_non_trivial_seed(seed: &[u8]) -> Result<()> {
+ if is_all_zero_or_ff(seed) {
+ return Err(KeystoreError::SeedError("invalid seed".to_string()));
+ }
+ Ok(())
+}
+
/// Derives an Ed25519 private key from a seed using SLIP-10 derivation.
///
/// This function implements the SLIP-10 specification for Ed25519 key derivation,
@@ -39,6 +46,7 @@ use crate::errors::{KeystoreError, Result};
/// ```
pub fn get_private_key_by_seed(seed: &[u8], path: &String) -> Result<[u8; 32]> {
+ ensure_non_trivial_seed(seed)?;
let mut i = get_master_key_by_seed(seed)?;
let path = normalize_path(path);
let derivation_path = DerivationPath::from_str(path.as_str())
@@ -112,6 +120,7 @@ pub fn get_public_key_by_seed(seed: &[u8], path: &String) -> Result<[u8; 32]> {
/// A 64-byte Ed25519 signature
///
pub fn sign_message_by_seed(seed: &[u8], path: &String, message: &[u8]) -> Result<[u8; 64]> {
+ ensure_non_trivial_seed(seed)?;
let mut secret_key = get_private_key_by_seed(seed, path)?;
let (mut keypair, _) = cryptoxide::ed25519::keypair(&secret_key);
let signature = cryptoxide::ed25519::signature(message, &keypair);
diff --git a/rust/keystore/src/algorithms/rsa/mod.rs b/rust/keystore/src/algorithms/rsa/mod.rs
index 1f062da..0c9b2bc 100644
--- a/rust/keystore/src/algorithms/rsa/mod.rs
+++ b/rust/keystore/src/algorithms/rsa/mod.rs
@@ -1,3 +1,4 @@
+use crate::algorithms::utils::is_all_zero_or_ff;
use crate::errors::{KeystoreError, Result};
use alloc::string::ToString;
@@ -19,6 +20,15 @@ pub const PRIME_LENGTH_IN_BYTE: usize = MODULUS_LENGTH / 8 / 2;
pub const MODULUS_LENGTH_IN_BYTE: usize = MODULUS_LENGTH / 8;
pub const SECRET_LENGTH_IN_BYTE: usize = PRIME_LENGTH_IN_BYTE * 2 + MODULUS_LENGTH_IN_BYTE * 2;
+fn ensure_non_trivial_seed(seed: &[u8]) -> Result<()> {
+ if is_all_zero_or_ff(seed) {
+ return Err(KeystoreError::GenerateSigningKeyError(
+ "invalid seed".to_string(),
+ ));
+ }
+ Ok(())
+}
+
fn get_rsa_seed(seed: &[u8]) -> Result<[u8; 32]> {
let mut intermediate;
let mut hash = seed;
@@ -33,6 +43,7 @@ fn get_rsa_seed(seed: &[u8]) -> Result<[u8; 32]> {
}
pub fn get_rsa_secret_from_seed(seed: &[u8]) -> Result<RsaPrivateKey> {
+ ensure_non_trivial_seed(seed)?;
// bip39 seed length is 64, slip39 seed length is 16 or 32
let seed_len = seed.len();
if !matches!(seed.len(), 16 | 32 | 64) {
diff --git a/rust/keystore/src/algorithms/secp256k1.rs b/rust/keystore/src/algorithms/secp256k1.rs
index 8cd13c3..01980cd 100644
--- a/rust/keystore/src/algorithms/secp256k1.rs
+++ b/rust/keystore/src/algorithms/secp256k1.rs
@@ -11,7 +11,7 @@ use bitcoin::secp256k1;
use bitcoin::Network;
use secp256k1::Message;
-use crate::algorithms::utils::normalize_path;
+use crate::algorithms::utils::{is_all_zero_or_ff, normalize_path};
use crate::errors::{KeystoreError, Result};
@@ -39,6 +39,7 @@ pub fn get_public_key_by_seed(seed: &[u8], path: &String) -> Result<PublicKey> {
}
fn get_extended_private_key_by_seed(seed: &[u8], path: &String) -> Result<Xpriv> {
+ ensure_non_trivial_seed(seed)?;
let p = normalize_path(path);
let derivation_path = DerivationPath::from_str(p.as_str())
.map_err(|e| KeystoreError::InvalidDerivationPath(e.to_string()))?;
@@ -54,16 +55,25 @@ pub fn get_extended_public_key_by_seed(seed: &[u8], path: &String) -> Result<Xpu
}
pub fn get_master_fingerprint_by_seed(seed: &[u8]) -> Result<Fingerprint> {
+ ensure_non_trivial_seed(seed)?;
let root = Xpriv::new_master(Network::Bitcoin, seed)
.map_err(|e| KeystoreError::SeedError(e.to_string()))?;
Ok(root.fingerprint(&secp256k1::Secp256k1::new()))
}
+fn ensure_non_trivial_seed(seed: &[u8]) -> Result<()> {
+ if is_all_zero_or_ff(seed) {
+ return Err(KeystoreError::SeedError("invalid seed".to_string()));
+ }
+ Ok(())
+}
+
pub fn sign_message_by_seed(
seed: &[u8],
path: &String,
message: &Message,
) -> Result<(i32, [u8; 64])> {
+ ensure_non_trivial_seed(seed)?;
let key = get_private_key_by_seed(seed, path)?;
let secp = secp256k1::Secp256k1::new();
let (rec_id, signature) = secp
diff --git a/rust/keystore/src/algorithms/utils.rs b/rust/keystore/src/algorithms/utils.rs
index 28bea7c..c95d28e 100644
--- a/rust/keystore/src/algorithms/utils.rs
+++ b/rust/keystore/src/algorithms/utils.rs
@@ -7,3 +7,10 @@ pub fn normalize_path(path: &str) -> String {
}
p
}
+
+pub fn is_all_zero_or_ff(bytes: &[u8]) -> bool {
+ if bytes.is_empty() {
+ return false;
+ }
+ bytes.iter().all(|b| *b == 0x00) || bytes.iter().all(|b| *b == 0xFF)
+}
diff --git a/rust/keystore/src/algorithms/zcash/mod.rs b/rust/keystore/src/algorithms/zcash/mod.rs
index 4c23976..6673b23 100644
--- a/rust/keystore/src/algorithms/zcash/mod.rs
+++ b/rust/keystore/src/algorithms/zcash/mod.rs
@@ -13,6 +13,7 @@ use zcash_vendor::{
zip32::{self, fingerprint::SeedFingerprint},
};
+use crate::algorithms::utils::is_all_zero_or_ff;
use crate::errors::{KeystoreError, Result};
pub fn derive_ufvk<P: consensus::Parameters>(
@@ -20,6 +21,7 @@ pub fn derive_ufvk<P: consensus::Parameters>(
seed: &[u8],
account_path: &str,
) -> Result<String> {
+ ensure_non_trivial_seed(seed)?;
let account_path = DerivationPath::from_str(account_path.to_lowercase().as_str())
.map_err(|e| KeystoreError::DerivationError(e.to_string()))?;
if account_path.len() != 3 {
@@ -50,7 +52,15 @@ pub fn derive_ufvk<P: consensus::Parameters>(
}
}
+fn ensure_non_trivial_seed(seed: &[u8]) -> Result<()> {
+ if is_all_zero_or_ff(seed) {
+ return Err(KeystoreError::SeedError("invalid seed".to_string()));
+ }
+ Ok(())
+}
+
pub fn calculate_seed_fingerprint(seed: &[u8]) -> Result<[u8; 32]> {
+ ensure_non_trivial_seed(seed)?;
let sfp = SeedFingerprint::from_seed(seed).ok_or(KeystoreError::SeedError(
"Invalid seed, cannot calculate ZIP-32 Seed Fingerprint".into(),
))?;
@@ -64,6 +74,7 @@ pub fn sign_message_orchard<R: RngCore + CryptoRng>(
path: &[zip32::ChildIndex],
rng: R,
) -> Result<()> {
+ ensure_non_trivial_seed(seed)?;
let coin_type = 133;
if path.len() == 3
diff --git a/rust/rust_c/src/ethereum/mod.rs b/rust/rust_c/src/ethereum/mod.rs
index b808ef6..c3d0ae1 100644
--- a/rust/rust_c/src/ethereum/mod.rs
+++ b/rust/rust_c/src/ethereum/mod.rs
@@ -484,7 +484,6 @@ pub unsafe extern "C" fn eth_sign_batch_tx(
}
};
- seed.zeroize();
match signature {
Err(e) => return UREncodeResult::from(e).c_ptr(),
Ok(sig) => {
@@ -497,6 +496,7 @@ pub unsafe extern "C" fn eth_sign_batch_tx(
}
}
}
+ seed.zeroize();
let ret = EthBatchSignature::new(result);
Why this scored 61/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.