primitives: Remove FromHex usage in serde code
What changed, and why it matters
This commit swaps out a custom hex-decoding helper for a simpler one in the code that turns serialized data back into Bitcoin witness structures. The change is driven by an upstream library hiding its internal error details, not by a known security flaw. The main practical effect is that error messages become less specific when someone feeds in bad hex data. There is no direct evidence this introduces a vulnerability, but it slightly weakens defensive error reporting.
Treat as routine maintenance. If downstream consumers rely on specific serde error variants for malformed witness hex, update them to handle generic custom errors. No security patch or incident response is indicated by the commit content.
Security signals we found
Loss of precise deserialization error reporting
Dependency API incompatibility fix
No input validation bypass observed
Evidence from the diff
The patch replaces hex_unstable::FromHex with hex::decode_to_vec in the serde Deserialize implementation for Witness. Previously, invalid hex characters and odd-length strings were mapped to precise serde::de::Error variants (invalid_value/invalid_length). Now any decoding failure is wrapped in a generic serde::de::Error::custom. This is a compatibility fix because the stable hex crate no longer exposes HexToBytesError internals. The functional behavior—rejecting malformed input—remains unchanged; only the diagnostic detail of the returned error is reduced.
Changed components
primitives/src/witness.rsWitness serde deserializationInspect captured patch +1 / −18
diff --git a/primitives/src/witness.rs b/primitives/src/witness.rs
index fc114b50..f103b6bd 100644
--- a/primitives/src/witness.rs
+++ b/primitives/src/witness.rs
@@ -733,30 +733,13 @@ impl<'de> serde::Deserialize<'de> for Witness {
self,
mut a: A,
) -> Result<Self::Value, A::Error> {
- use hex_unstable::{FromHex, HexToBytesError as E};
- use serde::de::{self, Unexpected};
-
let mut ret = match a.size_hint() {
Some(len) => Vec::with_capacity(len),
None => Vec::new(),
};
while let Some(elem) = a.next_element::<String>()? {
- let vec = Vec::<u8>::from_hex(&elem).map_err(|e| match e {
- E::InvalidChar(ref e) =>
- match core::char::from_u32(e.invalid_char().into()) {
- Some(c) => de::Error::invalid_value(
- Unexpected::Char(c),
- &"a valid hex character",
- ),
- None => de::Error::invalid_value(
- Unexpected::Unsigned(e.invalid_char().into()),
- &"a valid hex character",
- ),
- },
- E::OddLengthString(ref e) =>
- de::Error::invalid_length(e.length(), &"an even length string"),
- })?;
+ let vec = hex::decode_to_vec(&elem).map_err(serde::de::Error::custom)?;
ret.push(vec);
}
Ok(Witness::from_slice(&ret))
Why this scored 19/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.