Repository navigation
fix(crypto): deserialize legacy BLS public keys #1145
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| //! BLS public key bytes with compatibility for persisted binary Serde keys. | ||
|
|
||
| use core::{array::TryFromSliceError, fmt, str::FromStr}; | ||
|
|
||
| use dash_types::{impl_bytes, type_cvrt, type_id::TypeId, ParseHexError}; | ||
|
|
||
| use super::BLS_PK_LEN; | ||
|
|
||
| // Keep dash-types' byte API, formatting and codecs; only Serde decoding differs. | ||
| mod raw { | ||
| use super::BLS_PK_LEN; | ||
|
|
||
| dash_types::make_bytes! { | ||
| /// BLS public key bytes. | ||
| BlsPkBytes, BLS_PK_LEN | ||
| } | ||
| } | ||
|
|
||
| /// BLS public key (48 bytes, unvalidated). | ||
| #[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, TypeId)] | ||
| pub struct BlsPkBytes(raw::BlsPkBytes); | ||
|
|
||
| impl BlsPkBytes { | ||
| /// Wraps raw bytes without validation. | ||
| pub const fn from_bytes(bytes: [u8; BLS_PK_LEN]) -> Self { | ||
| Self(raw::BlsPkBytes::from_bytes(bytes)) | ||
| } | ||
|
|
||
| /// Copies out the inner byte array. | ||
| pub const fn to_bytes(&self) -> [u8; BLS_PK_LEN] { | ||
| self.0.to_bytes() | ||
| } | ||
|
|
||
| /// Borrows the inner byte array. | ||
| pub const fn as_bytes(&self) -> &[u8; BLS_PK_LEN] { | ||
| self.0.as_bytes() | ||
| } | ||
|
|
||
| /// Returns `true` when every byte is zero. | ||
| pub fn is_null(&self) -> bool { | ||
| self.0.is_null() | ||
| } | ||
| } | ||
|
|
||
| impl_bytes!(BlsPkBytes, BLS_PK_LEN); | ||
| type_cvrt!(From<[u8; BLS_PK_LEN]> for BlsPkBytes, |bytes| Self::from_bytes(*bytes)); | ||
|
|
||
| impl From<BlsPkBytes> for [u8; BLS_PK_LEN] { | ||
| fn from(key: BlsPkBytes) -> Self { | ||
| key.to_bytes() | ||
| } | ||
| } | ||
|
|
||
| impl TryFrom<&[u8]> for BlsPkBytes { | ||
| type Error = TryFromSliceError; | ||
|
|
||
| fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> { | ||
| raw::BlsPkBytes::try_from(bytes).map(Self) | ||
| } | ||
| } | ||
|
|
||
| impl AsRef<[u8]> for BlsPkBytes { | ||
| fn as_ref(&self) -> &[u8] { | ||
| self.as_bytes() | ||
| } | ||
| } | ||
|
|
||
| impl AsRef<[u8; BLS_PK_LEN]> for BlsPkBytes { | ||
| fn as_ref(&self) -> &[u8; BLS_PK_LEN] { | ||
| self.as_bytes() | ||
| } | ||
| } | ||
|
|
||
| impl FromStr for BlsPkBytes { | ||
| type Err = ParseHexError; | ||
|
|
||
| fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
| s.parse().map(Self) | ||
| } | ||
| } | ||
|
|
||
| macro_rules! delegate_format { | ||
| ($($trait:ident),+ $(,)?) => {$( | ||
| impl fmt::$trait for BlsPkBytes { | ||
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| fmt::$trait::fmt(&self.0, f) | ||
| } | ||
| } | ||
| )+}; | ||
| } | ||
| delegate_format!(Display, Debug, LowerHex, UpperHex); | ||
|
|
||
| #[cfg(feature = "serde")] | ||
| impl serde::Serialize for BlsPkBytes { | ||
| fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> { | ||
| self.0.serialize(serializer) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(feature = "serde")] | ||
| impl<'de> serde::Deserialize<'de> for BlsPkBytes { | ||
| fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> { | ||
| if deserializer.is_human_readable() { | ||
| return raw::BlsPkBytes::deserialize(deserializer).map(Self); | ||
| } | ||
|
|
||
| struct Visitor; | ||
| impl serde::de::Visitor<'_> for Visitor { | ||
| type Value = BlsPkBytes; | ||
|
|
||
| fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
| f.write_str("48 raw bytes or 96 ASCII hex digits for a BLS public key") | ||
| } | ||
|
|
||
| fn visit_str<E: serde::de::Error>(self, hex: &str) -> Result<Self::Value, E> { | ||
| hex.parse().map_err(E::custom) | ||
| } | ||
|
|
||
| fn visit_bytes<E: serde::de::Error>(self, bytes: &[u8]) -> Result<Self::Value, E> { | ||
| match bytes.len() { | ||
| BLS_PK_LEN => BlsPkBytes::try_from(bytes).map_err(E::custom), | ||
| // Binary Serde strings and byte buffers share a length prefix in bincode. | ||
| len if len == 2 * BLS_PK_LEN => { | ||
| let hex = core::str::from_utf8(bytes).map_err(E::custom)?; | ||
| BlsPkBytes::from_hex(hex).map_err(E::custom) | ||
| } | ||
| len => Err(E::invalid_length(len, &self)), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| deserializer.deserialize_byte_buf(Visitor) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| # Legacy BLS binary Serde fixture | ||
|
|
||
| `bls-public-key.bin` was generated using rust-dashcore revision | ||
| `40268cc0402a8933ec539f16b2d634c4e25876ad`, before the BLS migration in #1036. | ||
| It encodes the synthetic public-key bytes `0x00` through `0x2f` as a | ||
| length-prefixed, 96-character hex string. It is a serialization fixture, | ||
| not a validated cryptographic point. | ||
|
|
||
| To reproduce, use a standalone Cargo package with `dashcore` pointing at that | ||
| exact revision with its `serde` feature enabled, and | ||
| `bincode = { package = "grovedb-bincode", version = "=2.1.0", features = ["serde"] }`: | ||
|
|
||
| ```rust | ||
| let key = dashcore::bls_sig_utils::BLSPublicKey::from( | ||
| std::array::from_fn::<_, 48, _>(|i| i as u8), | ||
| ); | ||
| let bytes = bincode::serde::encode_to_vec(key, bincode::config::standard()).unwrap(); | ||
| std::fs::write("bls-public-key.bin", bytes).unwrap(); | ||
| ``` | ||
|
|
||
| Do not regenerate this fixture with current types. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| `000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| #![cfg(all(feature = "serde", feature = "bincode"))] | ||
|
|
||
| use dashcore_crypto::bls::BlsPkBytes; | ||
|
|
||
| #[test] | ||
| fn should_decode_both_bls_public_key_formats() { | ||
| let legacy = include_bytes!("data/legacy-serde/bls-public-key.bin"); | ||
| let expected = BlsPkBytes::from_bytes(std::array::from_fn(|i| i as u8)); | ||
| let (key, consumed): (BlsPkBytes, _) = | ||
| bincode::serde::decode_from_slice(legacy, bincode::config::standard()).unwrap(); | ||
| assert_eq!(key, expected); | ||
| assert_eq!(consumed, legacy.len()); | ||
| let current = bincode::serde::encode_to_vec(key, bincode::config::standard()).unwrap(); | ||
| assert_eq!(current[0], 48); | ||
| assert_eq!(¤t[1..], expected.as_bytes()); | ||
| let (key, consumed): (BlsPkBytes, _) = | ||
| bincode::serde::decode_from_slice(¤t, bincode::config::standard()).unwrap(); | ||
| assert_eq!(key, expected); | ||
| assert_eq!(consumed, current.len()); | ||
| assert_eq!( | ||
| serde_json::from_str::<BlsPkBytes>(&serde_json::to_string(&key).unwrap()).unwrap(), | ||
| key | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn should_reject_malformed_legacy_bls_public_keys() { | ||
| for bytes in [vec![b'g'; 96], vec![0xff; 96], vec![b'0'; 95], vec![b'0'; 97], vec![]] { | ||
| let encoded = bincode::serde::encode_to_vec(bytes, bincode::config::standard()).unwrap(); | ||
| assert!(bincode::serde::decode_from_slice::<BlsPkBytes, _>( | ||
| &encoded, | ||
| bincode::config::standard() | ||
| ) | ||
| .is_err()); | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn should_not_interpret_raw_bls_public_key_as_hex() { | ||
| let bytes = [b'a'; 48]; | ||
| let encoded = | ||
| bincode::serde::encode_to_vec(bytes.as_slice(), bincode::config::standard()).unwrap(); | ||
| let (key, _): (BlsPkBytes, _) = | ||
| bincode::serde::decode_from_slice(&encoded, bincode::config::standard()).unwrap(); | ||
| assert_eq!(key.as_bytes(), &bytes); | ||
| } | ||
|
|
||
| #[test] | ||
| fn should_decode_legacy_bls_with_fixed_big_endian_lengths() { | ||
| let config = bincode::config::standard().with_fixed_int_encoding().with_big_endian(); | ||
| let key = BlsPkBytes::from_bytes([0xab; 48]); | ||
| let bytes = bincode::serde::encode_to_vec(key.to_string().to_uppercase(), config).unwrap(); | ||
| let (decoded, consumed): (BlsPkBytes, _) = | ||
| bincode::serde::decode_from_slice(&bytes, config).unwrap(); | ||
| assert_eq!(decoded, key); | ||
| assert_eq!(consumed, bytes.len()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn should_preserve_native_bincode_public_key_encoding() { | ||
| let key = BlsPkBytes::from_bytes(std::array::from_fn(|i| i as u8)); | ||
| let bytes = bincode::encode_to_vec(key, bincode::config::standard()).unwrap(); | ||
| assert_eq!(bytes, key.as_bytes()); | ||
| let (decoded, consumed): (BlsPkBytes, _) = | ||
| bincode::decode_from_slice(&bytes, bincode::config::standard()).unwrap(); | ||
| assert_eq!(decoded, key); | ||
| assert_eq!(consumed, 48); | ||
| } | ||
|
|
||
| #[test] | ||
| fn should_reject_truncated_keys_and_respect_limits() { | ||
| let bytes = include_bytes!("data/legacy-serde/bls-public-key.bin"); | ||
| for end in 0..bytes.len() { | ||
| assert!(bincode::serde::decode_from_slice::<BlsPkBytes, _>( | ||
| &bytes[..end], | ||
| bincode::config::standard() | ||
| ) | ||
| .is_err()); | ||
| } | ||
| assert!(bincode::serde::decode_from_slice::<BlsPkBytes, _>( | ||
| bytes, | ||
| bincode::config::standard().with_limit::<16>() | ||
| ) | ||
| .is_err()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn should_decode_legacy_key_between_other_fields() { | ||
| let key = BlsPkBytes::from_bytes([0xab; 48]); | ||
| let config = bincode::config::standard(); | ||
| let bytes = bincode::serde::encode_to_vec((7_u32, key.to_string(), 1234_u64), config).unwrap(); | ||
| let (decoded, consumed): ((u32, BlsPkBytes, u64), _) = | ||
| bincode::serde::decode_from_slice(&bytes, config).unwrap(); | ||
| assert_eq!(decoded, (7, key, 1234)); | ||
| assert_eq!(consumed, bytes.len()); | ||
| } | ||
|
|
||
| #[test] | ||
| fn should_accept_binary_deserializers_that_deliver_strings() { | ||
| struct BinaryString<'a>(&'a str); | ||
|
|
||
| impl<'de> serde::Deserializer<'de> for BinaryString<'de> { | ||
| type Error = serde::de::value::Error; | ||
|
|
||
| fn is_human_readable(&self) -> bool { | ||
| false | ||
| } | ||
|
|
||
| fn deserialize_any<V: serde::de::Visitor<'de>>( | ||
| self, | ||
| visitor: V, | ||
| ) -> Result<V::Value, Self::Error> { | ||
| visitor.visit_borrowed_str(self.0) | ||
| } | ||
|
|
||
| serde::forward_to_deserialize_any! { | ||
| bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string | ||
| bytes byte_buf option unit unit_struct newtype_struct seq tuple | ||
| tuple_struct map struct enum identifier ignored_any | ||
| } | ||
| } | ||
|
|
||
| let hex = "ab".repeat(48); | ||
| let key = <BlsPkBytes as serde::Deserialize>::deserialize(BinaryString(&hex)).unwrap(); | ||
| assert_eq!(key.as_bytes(), &[0xab; 48]); | ||
| assert!(<BlsPkBytes as serde::Deserialize>::deserialize(BinaryString("invalid")).is_err()); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Request borrowed bytes instead of an owned byte buffer.
If a caller decodes an untrusted key with
bincode::serde::borrow_decode_from_sliceand the standard configuration,deserialize_byte_bufallocates aVec<u8>from the declared length beforevisit_bytescan reject it. A short input with a very large length prefix can therefore exhaust memory before deserialization returns an error. Usedeserialize_bytes(Visitor)here. The borrowed bincode decoder can then pass a slice without allocating; owned decoding still needs an appropriate limit at its input boundary. (docs.rs)Proposed change
📝 Committable suggestion
🤖 Prompt for AI Agents