From 1ca31383ed9c373940f092c4823905724e4064ec Mon Sep 17 00:00:00 2001 From: serhii-kaliuzhnyi-dev Date: Sun, 9 Mar 2025 13:13:38 +0200 Subject: [PATCH 1/3] fix(issue-88): Fix decoding asn1 code with empty bitstring --- crates/asn1-parser/src/string/bit_string.rs | 16 ++++++- crates/asn1-parser/tests/decode_encode.rs | 51 ++++++++++++++++++++- src/asn1/scheme/strings.rs | 37 +++++++++------ 3 files changed, 87 insertions(+), 17 deletions(-) diff --git a/crates/asn1-parser/src/string/bit_string.rs b/crates/asn1-parser/src/string/bit_string.rs index a0f2fd16..a5dd088e 100644 --- a/crates/asn1-parser/src/string/bit_string.rs +++ b/crates/asn1-parser/src/string/bit_string.rs @@ -32,7 +32,14 @@ impl BitString<'_> { } pub fn bits_amount(&self) -> usize { - (self.octets.as_ref().len() - 1) * 8 - usize::from(self.octets.as_ref()[0]) + if self.octets.is_empty() { + return 0; + } + + let data_len = self.octets.len() - 1; + let padding = usize::from(self.octets[0]); + + (data_len * 8).saturating_sub(padding) } /// Creates a new [BitString] from amount of bits and actual bits buffer @@ -100,7 +107,12 @@ impl<'data> Asn1ValueDecoder<'data> for BitString<'data> { fn decode(_: Tag, reader: &mut Reader<'data>) -> Asn1Result { let data = reader.read_remaining(); - let inner = if !data.is_empty() { + if data.is_empty() { + return Err(Error::from("BitString must have at least one byte (unused bits)")); + } + + let inner = if data.len() > 1 { + // Check len > 1 since first byte is unused bits let mut inner_reader = Reader::new(&data[1..]); inner_reader.set_next_id(reader.next_id()); inner_reader.set_offset(reader.full_offset() - data.len()); diff --git a/crates/asn1-parser/tests/decode_encode.rs b/crates/asn1-parser/tests/decode_encode.rs index a397def4..1b9fb391 100644 --- a/crates/asn1-parser/tests/decode_encode.rs +++ b/crates/asn1-parser/tests/decode_encode.rs @@ -1,6 +1,6 @@ use std::sync::Once; -use asn1_parser::{Asn1, Asn1Decoder, Asn1Encoder, Asn1Type, MetaInfo, ObjectIdentifier, Taggable}; +use asn1_parser::{Asn1, Asn1Decoder, Asn1Encoder, Asn1Type, BitString, MetaInfo, ObjectIdentifier, Tag, Taggable}; use prop_strategies::any_asn1_type; use proptest::proptest; @@ -305,3 +305,52 @@ fn test_2() { let asn1 = Asn1::decode_buff(raw).unwrap(); println!("{:?}", asn1); } + +#[test] +fn empty_bitstring() { + init_logging(); + + let raw = [0x03, 0x01, 0x00]; + let asn1 = Asn1::decode_buff(&raw).expect("Failed to decode empty BitString"); + + assert_eq!(asn1.inner_asn1().tag(), Tag::from(3), "Tag should be 0x03"); + + if let Asn1Type::BitString(bitstring) = asn1.inner_asn1() { + assert_eq!(bitstring.raw_bits(), &[0], "Raw bits should be [0]"); + assert_eq!(bitstring.bits_amount(), 0, "Bits amount should be 0"); + assert!(bitstring.inner().is_none(), "Inner should be None"); + } else { + panic!("Expected BitString type"); + } + + // Test encoding back + let mut encoded = vec![0; asn1.needed_buf_size()]; + asn1.encode_buff(&mut encoded) + .expect("Failed to encode empty BitString"); + assert_eq!(encoded, raw, "Encoded bytes should match original"); + + // Test creating an empty BitString directly + let empty_bits = BitString::from_raw_vec(0, vec![]).expect("Failed to create empty BitString"); + assert_eq!(empty_bits.raw_bits(), &[0], "Created raw bits should be [0]"); + assert_eq!(empty_bits.bits_amount(), 0, "Created bits amount should be 0"); + assert!(empty_bits.inner().is_none(), "Created inner should be None"); + + // Test invalid decoding: BitString with length 0 (invalid in DER) + let invalid_raw = [0x03, 0x00]; + assert!( + Asn1::decode_buff(&invalid_raw).is_err(), + "Decoding length 0 should fail" + ); + + // Test invalid creation: too many bits + assert!( + BitString::from_raw_vec(1, vec![]).is_err(), + "Creating with too many bits should fail" + ); + + // Test invalid creation: too many unused bits + assert!( + BitString::from_raw_vec(0, vec![0]).is_err(), + "Creating with excess unused bits should fail" + ); +} diff --git a/src/asn1/scheme/strings.rs b/src/asn1/scheme/strings.rs index 31b21b5f..30a9eee2 100644 --- a/src/asn1/scheme/strings.rs +++ b/src/asn1/scheme/strings.rs @@ -1,14 +1,14 @@ +use crate::asn1::node_options::NodeOptions; +use crate::asn1::scheme::build_asn1_schema; +use crate::asn1::HighlightAction; +use crate::common::RcSlice; use asn1_parser::{ OwnedBitString, OwnedBmpString, OwnedGeneralString, OwnedIA5String, OwnedNumericString, OwnedOctetString, OwnedPrintableString, OwnedRawAsn1EntityData, OwnedUtf8String, OwnedVisibleString, }; +use std::fmt::Write; use yew::{function_component, html, Callback, Html, Properties}; -use crate::asn1::node_options::NodeOptions; -use crate::asn1::scheme::build_asn1_schema; -use crate::asn1::HighlightAction; -use crate::common::RcSlice; - #[derive(PartialEq, Properties, Clone)] pub struct OctetStringNodeProps { pub node: OwnedOctetString, @@ -68,15 +68,24 @@ pub struct BitStringNodeProps { #[function_component(BitStringNode)] pub fn bit_string(props: &BitStringNodeProps) -> Html { - let bits = props.node.raw_bits()[1..] - .iter() - .map(|byte| format!("{:08b}", byte)) - .fold(String::new(), |mut ac, new| { - ac.push_str(&new); - ac - }); + let raw_bits = props.node.raw_bits(); let bits_amount = props.node.bits_amount(); - let bits = &bits[0..bits_amount]; + + let bits = if raw_bits.len() > 1 { + let mut bits = String::with_capacity((raw_bits.len() - 1) * 8); + for byte in &raw_bits[1..] { + write!(bits, "{:08b}", byte).unwrap(); + } + bits + } else { + String::new() + }; + + let display_bits = if bits.len() >= bits_amount { + &bits[0..bits_amount] + } else { + &bits[..] + }; let offset = props.meta.tag_position(); let length_len = props.meta.length_range().len(); @@ -99,7 +108,7 @@ pub fn bit_string(props: &BitStringNodeProps) -> Html {
{format!("({} bits)", bits_amount)} - {bits} + {display_bits}
} } From a8aa497586b06551f72e2ce955b3500422ba425c Mon Sep 17 00:00:00 2001 From: serhii-kaliuzhnyi-dev Date: Sat, 15 Mar 2025 18:03:18 +0200 Subject: [PATCH 2/3] Fix formatting --- src/asn1/scheme/strings.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/asn1/scheme/strings.rs b/src/asn1/scheme/strings.rs index 30a9eee2..9a2e37df 100644 --- a/src/asn1/scheme/strings.rs +++ b/src/asn1/scheme/strings.rs @@ -1,14 +1,16 @@ -use crate::asn1::node_options::NodeOptions; -use crate::asn1::scheme::build_asn1_schema; -use crate::asn1::HighlightAction; -use crate::common::RcSlice; +use std::fmt::Write; + use asn1_parser::{ OwnedBitString, OwnedBmpString, OwnedGeneralString, OwnedIA5String, OwnedNumericString, OwnedOctetString, OwnedPrintableString, OwnedRawAsn1EntityData, OwnedUtf8String, OwnedVisibleString, }; -use std::fmt::Write; use yew::{function_component, html, Callback, Html, Properties}; +use crate::asn1::node_options::NodeOptions; +use crate::asn1::scheme::build_asn1_schema; +use crate::asn1::HighlightAction; +use crate::common::RcSlice; + #[derive(PartialEq, Properties, Clone)] pub struct OctetStringNodeProps { pub node: OwnedOctetString, @@ -81,11 +83,7 @@ pub fn bit_string(props: &BitStringNodeProps) -> Html { String::new() }; - let display_bits = if bits.len() >= bits_amount { - &bits[0..bits_amount] - } else { - &bits[..] - }; + let display_bits = &bits[0..bits_amount.min(bits.len())]; let offset = props.meta.tag_position(); let length_len = props.meta.length_range().len(); From c5d3eaf73303552932cfb514990e4ff1fd7320cd Mon Sep 17 00:00:00 2001 From: serhii-kaliuzhnyi-dev Date: Sat, 15 Mar 2025 18:39:45 +0200 Subject: [PATCH 3/3] Remove octets validation --- crates/asn1-parser/src/string/bit_string.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/asn1-parser/src/string/bit_string.rs b/crates/asn1-parser/src/string/bit_string.rs index a5dd088e..ab2099e8 100644 --- a/crates/asn1-parser/src/string/bit_string.rs +++ b/crates/asn1-parser/src/string/bit_string.rs @@ -32,10 +32,6 @@ impl BitString<'_> { } pub fn bits_amount(&self) -> usize { - if self.octets.is_empty() { - return 0; - } - let data_len = self.octets.len() - 1; let padding = usize::from(self.octets[0]);