What changed, and why it matters
This commit adds a new no-allocator base58 decoder for short fixed-size data in the rust-bitcoin base58 crate. It is a feature addition, not a fix for a known vulnerability. The change exposes a new public function and error type and restructures some internal error gating. There is no direct evidence in the commit that this resolves an active security issue, but any new decoder handling checksums and length checks carries ordinary implementation-risk.
Treat as a routine feature commit with normal code-review risk. Reviewers should verify that the 128-character bound and the leading-zero + scratch length arithmetic cannot underflow or produce an out-of-range subslice, and confirm that DecodeCheckArrayError's public API surface is intended for no-alloc consumers. No urgent security action is indicated by the diff alone.
Security signals we found
New checksum-verifying decoder added to no-alloc code path
Error type gating changed: several error types now compiled without alloc as pub(crate)
Input length capped at 128 characters to bound stack buffer
Final array conversion uses expect after explicit length equality check
No vendor statement of security relevance or CVE in commit message
Evidence from the diff
The patch introduces decode_check_to_array
Changed components
base58/src/lib.rsbase58/src/error.rsInspect captured patch +184 / −28
diff --git a/base58/src/error.rs b/base58/src/error.rs
index 9a6917ba..f2f5a850 100644
--- a/base58/src/error.rs
+++ b/base58/src/error.rs
@@ -5,7 +5,6 @@
use core::convert::Infallible;
use core::fmt;
-#[cfg(feature = "alloc")]
use internals::write_err;
/// An error occurred during base58 decoding (with checksum).
@@ -13,7 +12,11 @@ use internals::write_err;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error(pub(super) ErrorInner);
-#[cfg(feature = "alloc")]
+#[cfg(not(feature = "alloc"))]
+/// An error occurred during base58 decoding (with checksum).
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct Error(pub(super) ErrorInner);
+
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum ErrorInner {
/// Invalid character while decoding.
@@ -24,7 +27,6 @@ pub(super) enum ErrorInner {
TooShort(TooShortError),
}
-#[cfg(feature = "alloc")]
impl Error {
/// Returns the invalid base58 character, if encountered.
pub fn invalid_character(&self) -> Option<u8> {
@@ -51,12 +53,10 @@ impl Error {
}
}
-#[cfg(feature = "alloc")]
impl From<Infallible> for Error {
fn from(never: Infallible) -> Self { match never {} }
}
-#[cfg(feature = "alloc")]
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ErrorInner::{Decode, IncorrectChecksum, TooShort};
@@ -82,22 +82,18 @@ impl std::error::Error for Error {
}
}
-#[cfg(feature = "alloc")]
impl From<InvalidCharacterError> for Error {
fn from(e: InvalidCharacterError) -> Self { Self(ErrorInner::Decode(e)) }
}
-#[cfg(feature = "alloc")]
impl From<IncorrectChecksumError> for Error {
fn from(e: IncorrectChecksumError) -> Self { Self(ErrorInner::IncorrectChecksum(e)) }
}
-#[cfg(feature = "alloc")]
impl From<TooShortError> for Error {
fn from(e: TooShortError) -> Self { Self(ErrorInner::TooShort(e)) }
}
-#[cfg(feature = "alloc")]
/// Checksum was not correct.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct IncorrectChecksumError {
@@ -107,12 +103,10 @@ pub(super) struct IncorrectChecksumError {
pub(super) expected: u32,
}
-#[cfg(feature = "alloc")]
impl From<Infallible> for IncorrectChecksumError {
fn from(never: Infallible) -> Self { match never {} }
}
-#[cfg(feature = "alloc")]
impl fmt::Display for IncorrectChecksumError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
@@ -131,7 +125,6 @@ impl std::error::Error for IncorrectChecksumError {
}
}
-#[cfg(feature = "alloc")]
/// The decoded base58 data was too short (require at least 4 bytes for checksum).
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct TooShortError {
@@ -139,12 +132,10 @@ pub(super) struct TooShortError {
pub(super) length: usize,
}
-#[cfg(feature = "alloc")]
impl From<Infallible> for TooShortError {
fn from(never: Infallible) -> Self { match never {} }
}
-#[cfg(feature = "alloc")]
impl fmt::Display for TooShortError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
@@ -202,32 +193,131 @@ impl std::error::Error for InputTooLongError {
}
}
+/// Error returned when decoding base58check data into a fixed-size array.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct DecodeCheckArrayError(pub(super) DecodeCheckArrayErrorInner);
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(super) enum DecodeCheckArrayErrorInner {
+ /// Decoding the base58check string failed (invalid character, bad checksum or too short).
+ Decode(Error),
+ /// The decoded payload length did not match the requested array length.
+ UnexpectedLength(UnexpectedLengthError),
+}
+
+impl DecodeCheckArrayError {
+ /// Returns the invalid base58 character, if encountered.
+ pub fn invalid_character(&self) -> Option<u8> {
+ match self.0 {
+ DecodeCheckArrayErrorInner::Decode(ref e) => e.invalid_character(),
+ DecodeCheckArrayErrorInner::UnexpectedLength(_) => None,
+ }
+ }
+
+ /// Returns the incorrect checksum along with the expected checksum, if encountered.
+ pub fn incorrect_checksum(&self) -> Option<(u32, u32)> {
+ match self.0 {
+ DecodeCheckArrayErrorInner::Decode(ref e) => e.incorrect_checksum(),
+ DecodeCheckArrayErrorInner::UnexpectedLength(_) => None,
+ }
+ }
+
+ /// Returns the invalid base58 string length (require at least 4 bytes for checksum), if encountered.
+ pub fn invalid_length(&self) -> Option<usize> {
+ match self.0 {
+ DecodeCheckArrayErrorInner::Decode(ref e) => e.invalid_length(),
+ DecodeCheckArrayErrorInner::UnexpectedLength(_) => None,
+ }
+ }
+}
+
+impl From<Infallible> for DecodeCheckArrayError {
+ fn from(never: Infallible) -> Self { match never {} }
+}
+
+impl fmt::Display for DecodeCheckArrayError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use DecodeCheckArrayErrorInner::{Decode, UnexpectedLength};
+
+ match self.0 {
+ Decode(ref e) => write_err!(f, "decode"; e),
+ UnexpectedLength(ref e) => write_err!(f, "unexpected length"; e),
+ }
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for DecodeCheckArrayError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ use DecodeCheckArrayErrorInner::{Decode, UnexpectedLength};
+
+ match self.0 {
+ Decode(ref e) => Some(e),
+ UnexpectedLength(ref e) => Some(e),
+ }
+ }
+}
+
+/// The decoded base58check payload length did not match the requested array length.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(super) struct UnexpectedLengthError {
+ /// The requested (expected) array length.
+ pub(super) expected: usize,
+ /// The decoded payload length.
+ ///
+ /// When the input string exceeds the maximum supported length, this is an
+ /// approximation of the decoded length derived from the input string length.
+ pub(super) actual: usize,
+}
+
+impl From<Infallible> for UnexpectedLengthError {
+ fn from(never: Infallible) -> Self { match never {} }
+}
+
+impl fmt::Display for UnexpectedLengthError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(
+ f,
+ "decoded base58check payload was {} bytes, expected {}",
+ self.actual, self.expected
+ )
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for UnexpectedLengthError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ let Self { expected: _, actual: _ } = self;
+ None
+ }
+}
+
/// Found an invalid ASCII byte while decoding base58 string.
#[cfg(feature = "alloc")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidCharacterError(pub(super) InvalidCharacterErrorInner);
-#[cfg(feature = "alloc")]
+#[cfg(not(feature = "alloc"))]
+/// Found an invalid ASCII byte while decoding base58 string.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub(crate) struct InvalidCharacterError(pub(super) InvalidCharacterErrorInner);
+
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct InvalidCharacterErrorInner {
pub(super) invalid: u8,
}
-#[cfg(feature = "alloc")]
impl InvalidCharacterError {
- #[cfg(feature = "alloc")]
pub(super) fn new(invalid: u8) -> Self { Self(InvalidCharacterErrorInner { invalid }) }
/// Returns the invalid base58 character.
pub fn invalid_character(&self) -> u8 { self.0.invalid }
}
-#[cfg(feature = "alloc")]
impl From<Infallible> for InvalidCharacterError {
fn from(never: Infallible) -> Self { match never {} }
}
-#[cfg(feature = "alloc")]
impl fmt::Display for InvalidCharacterError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "invalid base58 character {:#x}", self.0.invalid)
@@ -242,7 +332,6 @@ impl std::error::Error for InvalidCharacterError {
}
}
-#[cfg(feature = "alloc")]
/// An error that can occur from the `build_base256` function.
pub(super) enum Base256Error<T> {
Buffer(T),
diff --git a/base58/src/lib.rs b/base58/src/lib.rs
index 59ef2c17..314ad635 100644
--- a/base58/src/lib.rs
+++ b/base58/src/lib.rs
@@ -40,27 +40,26 @@ use core::fmt;
pub use std::{string::String, vec::Vec};
use hashes::sha256d;
-#[cfg(feature = "alloc")]
use internals::array::ArrayExt;
use internals::array_vec::ArrayVec;
#[allow(unused)] // MSRV polyfill
-#[cfg(feature = "alloc")]
use internals::slice::SliceExt;
+use crate::error::{
+ Base256Error, DecodeCheckArrayErrorInner, IncorrectChecksumError, TooShortError,
+ UnexpectedLengthError,
+};
#[cfg(not(feature = "alloc"))]
-use crate::error::InputTooLongErrorInner;
-#[cfg(feature = "alloc")]
-use crate::error::{Base256Error, IncorrectChecksumError, TooShortError};
+use crate::error::{Error, InputTooLongErrorInner, InvalidCharacterError};
#[rustfmt::skip] // Keep public re-exports separate.
#[cfg(feature = "alloc")]
#[doc(no_inline)]
pub use self::error::{Error, InvalidCharacterError};
#[doc(no_inline)]
-pub use self::error::InputTooLongError;
+pub use self::error::{DecodeCheckArrayError, InputTooLongError};
#[rustfmt::skip]
-#[cfg(feature = "alloc")]
static BASE58_DIGITS: [Option<u8>; 128] = [
None, None, None, None, None, None, None, None, // 0-7
None, None, None, None, None, None, None, None, // 8-15
@@ -88,7 +87,6 @@ static BASE58_DIGITS: [Option<u8>; 128] = [
/// # Errors
///
/// Returns an error if the input contains an invalid base58 character (not in the base58 alphabet).
-#[cfg(feature = "alloc")]
fn build_base256<T: Buffer>(data: &str, scratch: &mut T) -> Result<(), Base256Error<T::Err>> {
// Build in base 256
for d58 in data.bytes() {
@@ -163,6 +161,75 @@ pub fn decode_check(data: &str) -> Result<Vec<u8>, Error> {
Ok(ret)
}
+/// Decodes a base58check-encoded string into a fixed-size array, verifying the checksum.
+///
+/// This does not require `alloc`, but it only works for inputs up to 128 characters long. `N` is
+/// the expected length of the decoded payload (excluding the 4 byte checksum). Decoding will fail if
+/// the payload is any other length.
+///
+/// # Errors
+///
+/// * The input contains an invalid base58 character.
+/// * The decoded data is less than 4 bytes (too short for checksum verification).
+/// * The checksum does not match the expected value.
+/// * The input is longer than 128 characters.
+/// * The decoded payload length is not exactly `N` bytes.
+#[allow(clippy::missing_panics_doc)] // payload length is checked before cast unwrap
+pub fn decode_check_to_array<const N: usize>(data: &str) -> Result<[u8; N], DecodeCheckArrayError> {
+ // 11/15 is just over log_256(58), so the decoded length never exceeds the input length.
+ let mut scratch = ArrayVec::<u8, SHORT_OPT_BUFFER_LEN>::new();
+ build_base256(data, &mut scratch)
+ .map_err(|e| match e {
+ // Too long to decode within the fixed buffer. Report an approximate decoded length.
+ Base256Error::Buffer(_) =>
+ DecodeCheckArrayErrorInner::UnexpectedLength(UnexpectedLengthError {
+ expected: N,
+ actual: data.len() * 11 / 15,
+ }),
+ Base256Error::InvalidChar(err) => DecodeCheckArrayErrorInner::Decode(Error::from(err)),
+ })
+ .map_err(DecodeCheckArrayError)?;
+
+ let leading_zeros = data.bytes().take_while(|&x| x == BASE58_CHARS[0]).count();
+ let decoded_len = leading_zeros + scratch.len();
+
+ let mut decoded = [0u8; SHORT_OPT_BUFFER_LEN];
+ scratch.as_mut_slice().reverse();
+
+ // Copy the scratch into a subslice, erroring if out of range.
+ let write_slice = decoded
+ .get_mut(leading_zeros..decoded_len)
+ .ok_or(UnexpectedLengthError { expected: N, actual: data.len() * 11 / 15 })
+ .map_err(DecodeCheckArrayErrorInner::UnexpectedLength)
+ .map_err(DecodeCheckArrayError)?;
+ write_slice.copy_from_slice(&scratch);
+ let decoded = &decoded[..decoded_len];
+
+ let (payload, &data_check) = decoded.split_last_chunk::<4>().ok_or_else(|| {
+ DecodeCheckArrayError(DecodeCheckArrayErrorInner::Decode(Error::from(TooShortError {
+ length: decoded_len,
+ })))
+ })?;
+
+ if payload.len() != N {
+ return Err(DecodeCheckArrayError(DecodeCheckArrayErrorInner::UnexpectedLength(
+ UnexpectedLengthError { expected: N, actual: payload.len() },
+ )));
+ }
+
+ let hash_check = *sha256d::Hash::hash(payload).as_byte_array().sub_array::<0, 4>();
+ let expected = u32::from_le_bytes(hash_check);
+ let actual = u32::from_le_bytes(data_check);
+
+ if actual != expected {
+ return Err(DecodeCheckArrayError(DecodeCheckArrayErrorInner::Decode(Error::from(
+ IncorrectChecksumError { incorrect: actual, expected },
+ ))));
+ }
+
+ Ok(payload.try_into().expect("payload length checked to equal N"))
+}
+
const SHORT_OPT_BUFFER_LEN: usize = 128;
/// A base58check-encoded string (data followed by a 4 byte `SHA256d` checksum, base58-encoded).
Why this scored 21/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.