Repository: alex/rust-asn1 (language: rust)

Issue:
Support BMPString type
BMPString is allowed in `DisplayText` in RFC 5280 and is seen in the wild. Validating the bytes but only having a method to get bytes (rather than providing a `str`) would be sufficient for the cryptography use case.


Discussion / hints:

Fix patch:
diff --git a/fuzz/fuzz_targets/fuzz_asn1_parse.rs b/fuzz/fuzz_targets/fuzz_asn1_parse.rs
index 4bf2789..12b2128 100644
--- a/fuzz/fuzz_targets/fuzz_asn1_parse.rs
+++ b/fuzz/fuzz_targets/fuzz_asn1_parse.rs
@@ -14,6 +14,7 @@ fuzz_target!(|data: &[u8]| {
 
         d.read_element::<&[u8]>()?;
         d.read_element::<asn1::PrintableString>()?;
+        d.read_element::<asn1::BMPString>()?;
         d.read_element::<asn1::ObjectIdentifier>()?;
         d.read_element::<asn1::BitString>()?;
         d.read_element::<asn1::UtcTime>()?;
diff --git a/src/lib.rs b/src/lib.rs
index 1195b20..6650517 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -111,10 +111,10 @@ pub use crate::parser::{
     parse, parse_single, ParseError, ParseErrorKind, ParseLocation, ParseResult, Parser,
 };
 pub use crate::types::{
-    Asn1Readable, Asn1Writable, BigUint, Choice1, Choice2, Choice3, Enumerated, GeneralizedTime,
-    IA5String, Null, PrintableString, Sequence, SequenceOf, SequenceOfWriter, SequenceWriter,
-    SetOf, SetOfWriter, SimpleAsn1Readable, SimpleAsn1Writable, Tlv, UtcTime, Utf8String,
-    VisibleString,
+    Asn1Readable, Asn1Writable, BMPString, BigUint, Choice1, Choice2, Choice3, Enumerated,
+    GeneralizedTime, IA5String, Null, PrintableString, Sequence, SequenceOf, SequenceOfWriter,
+    SequenceWriter, SetOf, SetOfWriter, SimpleAsn1Readable, SimpleAsn1Writable, Tlv, UtcTime,
+    Utf8String, VisibleString,
 };
 #[cfg(feature = "const-generics")]
 pub use crate::types::{Explicit, Implicit};
diff --git a/src/parser.rs b/src/parser.rs
index f2f3b50..409c258 100644
--- a/src/parser.rs
+++ b/src/parser.rs
@@ -267,9 +267,9 @@ mod tests {
     use super::Parser;
     use crate::types::Asn1Readable;
     use crate::{
-        BigUint, BitString, Choice1, Choice2, Choice3, Enumerated, GeneralizedTime, IA5String,
-        ObjectIdentifier, ParseError, ParseErrorKind, ParseLocation, ParseResult, PrintableString,
-        Sequence, SequenceOf, SetOf, Tlv, UtcTime, Utf8String, VisibleString,
+        BMPString, BigUint, BitString, Choice1, Choice2, Choice3, Enumerated, GeneralizedTime,
+        IA5String, ObjectIdentifier, ParseError, ParseErrorKind, ParseLocation, ParseResult,
+        PrintableString, Sequence, SequenceOf, SetOf, Tlv, UtcTime, Utf8String, VisibleString,
     };
     #[cfg(feature = "const-generics")]
     use crate::{Explicit, Implicit};
@@ -709,6 +709,28 @@ mod tests {
         ])
     }
 
+    #[test]
+    fn test_parse_bmpstring() {
+        assert_parses::<BMPString>(&[
+            (
+                Ok(BMPString::new(b"\x00a\x00b\x00c").unwrap()),
+                b"\x1e\x06\x00a\x00b\x00c",
+            ),
+            (
+                Err(ParseError::new(ParseErrorKind::InvalidValue)),
+                b"\x1e\x01a",
+            ),
+            (
+                Err(ParseError::new(ParseErrorKind::InvalidValue)),
+                b"\x1e\x04\xde|X@",
+            ),
+            (
+                Err(ParseError::new(ParseErrorKind::InvalidValue)),
+                b"\x1e\x02\xdeX",
+            ),
+        ]);
+    }
+
     #[test]
     fn test_parse_utctime() {
         assert_parses::<UtcTime>(&[
diff --git a/src/types.rs b/src/types.rs
index f94704f..5cd1c76 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -385,6 +385,56 @@ impl<'a> SimpleAsn1Writable<'a> for VisibleString<'a> {
     }
 }
 
+/// Type for use with `Parser.read_element` and `Writer.write_element` for
+/// handling ASN.1 `BMPString`. A `BMPString` contains encoded (UTF-16-BE)
+/// bytes which are known to be valid.
+#[derive(Debug, PartialEq, Clone)]
+pub struct BMPString<'a>(&'a [u8]);
+
+impl<'a> BMPString<'a> {
+    pub fn new(b: &'a [u8]) -> Option<BMPString<'a>> {
+        if BMPString::verify(b) {
+            Some(BMPString(b))
+        } else {
+            None
+        }
+    }
+
+    fn verify(b: &[u8]) -> bool {
+        if b.len() % 2 == 1 {
+            return false;
+        }
+
+        for r in core::char::decode_utf16(
+            b.chunks_exact(2)
+                .map(|v| ((v[0] as u16) << 8) | (v[1] as u16)),
+        ) {
+            if r.is_err() {
+                return false;
+            }
+        }
+
+        true
+    }
+
+    pub fn as_utf16_be_bytes(&self) -> &'a [u8] {
+        self.0
+    }
+}
+
+impl<'a> SimpleAsn1Readable<'a> for BMPString<'a> {
+    const TAG: u8 = 0x1e;
+    fn parse_data(data: &'a [u8]) -> ParseResult<Self> {
+        BMPString::new(data).ok_or_else(|| ParseError::new(ParseErrorKind::InvalidValue))
+    }
+}
+impl<'a> SimpleAsn1Writable<'a> for BMPString<'a> {
+    const TAG: u8 = 0x1e;
+    fn write_data(&self, dest: &mut Vec<u8>) {
+        dest.extend_from_slice(self.as_utf16_be_bytes());
+    }
+}
+
 fn validate_integer(data: &[u8], signed: bool) -> ParseResult<()> {
     if data.is_empty() {
         return Err(ParseError::new(ParseErrorKind::InvalidValue));
diff --git a/src/writer.rs b/src/writer.rs
index 0192567..c4a7253 100644
--- a/src/writer.rs
+++ b/src/writer.rs
@@ -132,9 +132,9 @@ mod tests {
     use super::{_insert_at_position, write, write_single, Writer};
     use crate::types::Asn1Writable;
     use crate::{
-        parse_single, BigUint, BitString, Choice1, Choice2, Choice3, Enumerated, GeneralizedTime,
-        IA5String, ObjectIdentifier, OwnedBitString, PrintableString, Sequence, SequenceOf,
-        SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter, Tlv, UtcTime, Utf8String,
+        parse_single, BMPString, BigUint, BitString, Choice1, Choice2, Choice3, Enumerated,
+        GeneralizedTime, IA5String, ObjectIdentifier, OwnedBitString, PrintableString, Sequence,
+        SequenceOf, SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter, Tlv, UtcTime, Utf8String,
         VisibleString,
     };
     #[cfg(feature = "const-generics")]
@@ -237,6 +237,14 @@ mod tests {
         ]);
     }
 
+    #[test]
+    fn test_write_bmpstring() {
+        assert_writes::<BMPString>(&[(
+            BMPString::new(b"\x00a\x00b\x00c").unwrap(),
+            b"\x1e\x06\x00a\x00b\x00c",
+        )])
+    }
+
     #[test]
     fn test_write_i64() {
         assert_writes::<i64>(&[


Test patch: