diff --git a/.github/workflows/der.yml b/.github/workflows/der.yml index 8bc049587..b4d978b09 100644 --- a/.github/workflows/der.yml +++ b/.github/workflows/der.yml @@ -37,7 +37,7 @@ jobs: toolchain: ${{ matrix.rust }} targets: ${{ matrix.target }} - uses: RustCrypto/actions/cargo-hack-install@master - - run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features arbitrary,std + - run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features arbitrary,std,clarify minimal-versions: if: false # TODO: temp disabled due to unpublished prerelease dependencies diff --git a/der/Cargo.toml b/der/Cargo.toml index 3789d5370..57bff1449 100644 --- a/der/Cargo.toml +++ b/der/Cargo.toml @@ -26,6 +26,7 @@ pem-rfc7468 = { version = "1.0.0-rc.3", optional = true, features = ["alloc"] } time = { version = "0.3.4", optional = true, default-features = false } zeroize = { version = "1.8", optional = true, default-features = false } heapless = { version = "0.8", optional = true, default-features = false } +tynm = { version = "0.2", optional = true, default-features = false } [dev-dependencies] hex-literal = "1" @@ -41,6 +42,7 @@ derive = ["dep:der_derive"] oid = ["dep:const-oid"] pem = ["dep:pem-rfc7468", "alloc", "zeroize"] real = [] +clarify = ["dep:tynm", "std", "pem", "derive", "oid"] [package.metadata.docs.rs] all-features = true diff --git a/der/src/asn1/any.rs b/der/src/asn1/any.rs index efb891610..620934127 100644 --- a/der/src/asn1/any.rs +++ b/der/src/asn1/any.rs @@ -87,6 +87,7 @@ impl<'a> AnyRef<'a> { let mut decoder = SliceReader::new_with_encoding_rules(self.value(), encoding)?; let result = T::decode_value(&mut decoder, self.header())?; + decoder.finish()?; Ok(result) } diff --git a/der/src/encode.rs b/der/src/encode.rs index 578f7c9c3..ecc66fafb 100644 --- a/der/src/encode.rs +++ b/der/src/encode.rs @@ -81,7 +81,26 @@ where /// Encode this value as ASN.1 DER using the provided [`Writer`]. fn encode(&self, writer: &mut impl Writer) -> Result<()> { self.header()?.encode(writer)?; - self.encode_value(writer) + clarify_start_value_type::(writer); + let result = self.encode_value(writer); + clarify_end_value_type::(writer); + result + } +} + +#[allow(unused_variables)] +fn clarify_start_value_type(writer: &mut impl Writer) { + #[cfg(feature = "clarify")] + if let Some(clarifier) = writer.clarifier() { + clarifier.clarify_start_value_type::(); + } +} + +#[allow(unused_variables)] +fn clarify_end_value_type(writer: &mut impl Writer) { + #[cfg(feature = "clarify")] + if let Some(clarifier) = writer.clarifier() { + clarifier.clarify_end_value_type::(); } } diff --git a/der/src/header.rs b/der/src/header.rs index 7a4beabed..5a6a03117 100644 --- a/der/src/header.rs +++ b/der/src/header.rs @@ -61,8 +61,27 @@ impl Encode for Header { } fn encode(&self, writer: &mut impl Writer) -> Result<()> { + clarify_start_tag(writer, &self.tag); self.tag.encode(writer)?; - self.length.encode(writer) + let result = self.length.encode(writer); + clarify_end_length(writer, &self.tag, self.length); + result + } +} + +#[allow(unused_variables)] +fn clarify_start_tag(writer: &mut impl Writer, tag: &Tag) { + #[cfg(feature = "clarify")] + if let Some(clarifier) = writer.clarifier() { + clarifier.clarify_header_start_tag(tag); + } +} + +#[allow(unused_variables)] +fn clarify_end_length(writer: &mut impl Writer, tag: &Tag, length: Length) { + #[cfg(feature = "clarify")] + if let Some(clarifier) = writer.clarifier() { + clarifier.clarify_header_end_length(Some(tag), length); } } diff --git a/der/src/lib.rs b/der/src/lib.rs index d867ef928..d09c0f525 100644 --- a/der/src/lib.rs +++ b/der/src/lib.rs @@ -395,6 +395,11 @@ pub use { pem_rfc7468 as pem, }; +#[cfg(feature = "clarify")] +pub use writer::clarify::{ + Clarifier, ClarifyFlavor, ClarifyOptions, ClarifySliceWriter, EncodeClarifyExt, +}; + #[cfg(feature = "time")] pub use time; diff --git a/der/src/writer.rs b/der/src/writer.rs index 164b215f7..e59bb4ec4 100644 --- a/der/src/writer.rs +++ b/der/src/writer.rs @@ -1,5 +1,7 @@ //! Writer trait. +#[cfg(feature = "clarify")] +pub mod clarify; #[cfg(feature = "pem")] pub(crate) mod pem; pub(crate) mod slice; @@ -18,6 +20,12 @@ pub trait Writer { fn write_byte(&mut self, byte: u8) -> Result<()> { self.write(&[byte]) } + + #[cfg(feature = "clarify")] + /// Should return Some(clarifier) for clarify writers + fn clarifier(&mut self) -> Option<&mut clarify::Clarifier> { + None + } } #[cfg(feature = "std")] diff --git a/der/src/writer/clarify.rs b/der/src/writer/clarify.rs new file mode 100644 index 000000000..c730965e7 --- /dev/null +++ b/der/src/writer/clarify.rs @@ -0,0 +1,391 @@ +pub(crate) mod commentwriter; +pub(crate) mod hexdisplaylines; + +use crate::std::io::Write; +use crate::writer::clarify::commentwriter::RustHexWriter; +use crate::{Encode, Error, SliceWriter}; +use crate::{Length, Result, Tag}; +use commentwriter::{CommentWriter, JavaCommentWriter, XmlCommentWriter}; +use hexdisplaylines::HexDisplayLines; +use std::borrow::Cow; +use std::string::String; +use std::{boxed::Box, vec::Vec}; + +use super::Writer; + +/// Extension trait, auto-implemented on [`Encode`] +pub trait EncodeClarifyExt: Encode { + /// Encode this type as pretty-printed hex DER, with comments. + fn to_der_clarify(&self, flavor: ClarifyFlavor) -> Result { + let outputs = self.to_der_clarify_err_ignorant(ClarifyOptions { + flavor, + ..Default::default() + }); + // Propagate encode and finish errors + outputs.raw?; + Ok(String::from_utf8(outputs.clarify_buf).expect("clarified output to be utf-8")) + } + + /// Encode this type as pretty-printed hex DER, with comments. + /// Ignores any errors that occur during [`Encode::encode`]. + fn to_der_clarify_err_ignorant(&self, options: ClarifyOptions) -> ClarifyOutputs<'static> { + let len = match self.encoded_len() { + Ok(len) => len, + Err(err) => return ClarifyOutputs::from_err(err), + }; + + let mut buf = vec![0u8; u32::from(len) as usize]; + + let mut writer = ClarifySliceWriter::new(&mut buf, Vec::new(), options); + let result = self.encode(&mut writer); + + let outputs = writer.finish(); + let outputs = ClarifyOutputs { + // prioritize Encode::encode errors + raw: result.and(outputs.raw), + // but use buffer from finish() (even if encode failed) + clarify_buf: outputs.clarify_buf, + }; + + outputs.into_owned() + } +} + +impl EncodeClarifyExt for T where T: Encode {} + +/// Options to customize pretty-printing. +#[derive(Clone)] +pub struct ClarifyOptions { + /// How should comments look like? + pub flavor: ClarifyFlavor, + + /// Write types? E.g `type: OctetStringRef` + pub print_types: bool, +} + +impl Default for ClarifyOptions { + fn default() -> Self { + Self { + flavor: Default::default(), + print_types: true, + } + } +} + +static INDENT_STR: &str = + "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t"; + +/// [`Writer`] which encodes DER as hex with comments. +pub struct ClarifySliceWriter<'a> { + writer: SliceWriter<'a>, + + clarifier: Clarifier, +} + +/// Clarifier that creates HEX with comments +pub struct Clarifier { + /// Buffer into which debug HEX and comments are written + clarify_buf: Vec, + + /// Position in the buffer is used to track how long is the current sub-message + last_position: u32, + + /// Used for debug indentation + /// + /// Pushes writer positions on the stack + depth: Vec>, + + /// Determines if newlines and indent are currently enabled + indent_enabled: bool, + + print_types: bool, + + /// Sans-io buffer for comments + comment_writer: Box, +} + +/// Returned by .finish() +pub struct ClarifyOutputs<'a> { + /// Raw DER/BER buffer + pub raw: Result>, + + /// Hex-encoded DER/BER with comments + pub clarify_buf: Vec, +} + +impl<'a> ClarifyOutputs<'a> { + pub fn from_err(err: Error) -> ClarifyOutputs<'static> { + ClarifyOutputs { + raw: Err(err), + clarify_buf: Vec::new(), + } + } + + pub fn into_owned(self) -> ClarifyOutputs<'static> { + ClarifyOutputs { + raw: self.raw.map(|raw| Cow::Owned(raw.into_owned())), + clarify_buf: self.clarify_buf, + } + } +} + +/// Determines how comments will look like +#[derive(Copy, Clone, Debug, Default)] +pub enum ClarifyFlavor { + /// `01 02 ` + XmlComments, + /// `01 02 // comment` + #[default] + JavaComments, + /// `"01 02" // comment` + RustHex, +} + +impl Clarifier { + /// Creates new Clarifier with buffer, that accumulates comments and hex bytes. + pub fn new(clarify_buf: Vec, options: ClarifyOptions) -> Self { + Self { + clarify_buf, + + last_position: 0, + depth: Vec::new(), + + indent_enabled: true, + + print_types: options.print_types, + + comment_writer: match options.flavor { + ClarifyFlavor::XmlComments => Box::new(XmlCommentWriter::default()), + ClarifyFlavor::JavaComments => Box::new(JavaCommentWriter::default()), + ClarifyFlavor::RustHex => Box::new(RustHexWriter::default()), + }, + } + } +} + +impl<'a> ClarifySliceWriter<'a> { + /// Create a new encoder with the given byte slice as a backing buffer. + pub fn new(bytes: &'a mut [u8], clarify_buf: Vec, options: ClarifyOptions) -> Self { + Self { + writer: SliceWriter::new(bytes), + clarifier: Clarifier::new(clarify_buf, options), + } + } + + /// Finish encoding to the buffer, returning a slice containing the data + /// written to the buffer. + pub fn finish(mut self) -> ClarifyOutputs<'a> { + self.clarifier.flush_line(); + + ClarifyOutputs { + raw: self.writer.finish().map(Cow::Borrowed), + clarify_buf: self.clarifier.clarify_buf, + } + } + + /// Reserve a portion of the internal buffer, updating the internal cursor + /// position and returning a mutable slice. + fn reserve(&mut self, len: impl TryInto) -> Result<&mut [u8]> { + self.writer.reserve(len) + } +} + +impl Clarifier { + /// Returns indentation, for example "\n\t" for depth == 1 + pub fn indent_str(&self) -> &'static str { + let ilen = self.depth.len(); + let ilen = ilen.min(INDENT_STR.len()); + &INDENT_STR[..ilen] + } + + /// Writes indent if it is currently enabled + pub fn write_clarify_indent_if_enabled(&mut self) { + if self.indent_enabled { + self.write_clarify_indent(); + } + } + + fn flush_line(&mut self) { + // if current line ends in space + if self + .clarify_buf + .last() + .map(|last| *last == b' ') + .unwrap_or_default() + { + // remove space + self.clarify_buf.pop(); + } + // write comment after hex + self.comment_writer.before_new_line(&mut self.clarify_buf); + } + + /// Writes indentation to debug output, for example "\n\t" for depth == 1 + pub fn write_clarify_indent(&mut self) { + self.flush_line(); + + let indent = self.indent_str(); + write!(&mut self.clarify_buf, "\n{indent}").ok(); + + // write e.g. '"' before hex + self.comment_writer.start_new_line(&mut self.clarify_buf); + } + + /// Writes hex bytes to debug output, for example "30 04 " + pub fn write_clarify_hex(&mut self, bytes: &[u8]) { + let indent = self.indent_str(); + write!( + &mut self.clarify_buf, + "{}", + HexDisplayLines { + bytes, + indent, + space: self.comment_writer.needs_newline_space() + } + ) + .ok(); + } + + /// Writes string to debug output, for example a comment "// SEQUENCE" + pub fn write_clarify_str(&mut self, s: &str) { + write!(&mut self.clarify_buf, "{s}").ok(); + } + /// Writes string to debug output, for example a comment: `// SEQUENCE: name` + pub fn write_clarify_type_str(&mut self, start_end: &str, type_name: &str) { + let comment = format!("{start_end}: {type_name} "); + self.comment_writer.comment(&comment); + } + + /// Writes string to debug output, for example a comment: `// "abc"` + pub fn write_clarify_value_quote(&mut self, type_name: &str, value: &[u8]) { + let contains_control = value.iter().any(|&c| c == 0x7F || (c < 0x20 && c != b'\n')); + + if value.len() > 2 && !contains_control { + let type_name = strip_transparent_types(type_name); + let comment = format!("{} {:?} ", type_name, String::from_utf8_lossy(value)); + self.comment_writer.comment(&comment); + } + } + + /// Writes int to debug output, for example a comment: `// integer: 16dec` + pub fn write_clarify_int(&mut self, value: i64) { + if !(0..10).contains(&value) { + let comment = format!("integer: {value}dec "); + self.comment_writer.comment(&comment); + } + } + + /// Writes e.g. `type: OctetString` + /// + /// Expected `writer_pos` input: `u32::from(self.writer.position())` + pub fn clarify_start_value_type_str(&mut self, writer_pos: Option, type_name: &str) { + self.indent_enabled = true; + self.depth.push(writer_pos); + + if self.print_types { + let type_name = strip_transparent_types(type_name); + self.write_clarify_type_str("type", &type_name); + } + } + + fn clarify_end_value_type_str(&mut self, writer_pos: Option, type_name: &str) { + let last_pos = self.depth.pop().unwrap_or(writer_pos); + + if let (Some(writer_pos), Some(last_pos)) = (writer_pos, last_pos) { + let diff = writer_pos - last_pos; + if diff < 16 { + // ignore short runs + return; + } + } + + if self.print_types { + let type_name = strip_transparent_types(type_name); + self.write_clarify_indent(); + self.write_clarify_type_str("end", type_name.as_ref()); + } + } + + /// for better tag-length pretty-printing inline + pub fn clarify_header_start_tag(&mut self, _tag: &Tag) { + self.write_clarify_indent(); + // just to print header bytes without indent + self.indent_enabled = false; + } + + /// Writes field name, i.e. field: `public_key` + /// + /// when used on Sequence field: + /// ```text + /// public_key: Option<&'a [u8]> + /// ``` + pub fn clarify_field_name(&mut self, field_name: &str) { + self.write_clarify_indent(); + self.write_clarify_type_str("field", field_name); + } + + /// Writes e.g. `// type: OctetString` + pub fn clarify_start_value_type(&mut self) { + self.clarify_start_value_type_str(Some(self.last_position), &tynm::type_name::()); + } + /// Writes e.g. `// end: OctetString` + pub fn clarify_end_value_type(&mut self) { + self.clarify_end_value_type_str(Some(self.last_position), &tynm::type_name::()); + } + + /// Writes e.g. `// tag: OCTET STRING len: 17` + pub fn clarify_header_end_length(&mut self, tag: Option<&Tag>, length: Length) { + self.indent_enabled = true; + if let Some(tag) = tag { + self.write_clarify_type_str("tag", &format!("{tag}")); + } + if u32::from(length) >= 10 { + self.write_clarify_type_str("len", &format!("{length}")); + } + } + + /// Writes pretty-printed `CHOICE name` + pub fn clarify_choice(&mut self, choice_name: &[u8]) { + self.write_clarify_indent(); + if let Ok(choice_name) = std::str::from_utf8(choice_name) { + self.write_clarify_type_str("CHOICE", choice_name); + } + } +} + +impl<'a> Writer for ClarifySliceWriter<'a> { + #[allow(clippy::cast_possible_truncation)] + fn write(&mut self, slice: &[u8]) -> Result<()> { + self.reserve(slice.len())?.copy_from_slice(slice); + self.clarifier.last_position += slice.len() as u32; + + self.clarifier.write_clarify_indent_if_enabled(); + self.clarifier.write_clarify_hex(slice); + + Ok(()) + } + + fn clarifier(&mut self) -> Option<&mut Clarifier> { + Some(&mut self.clarifier) + } +} + +/// Strips wrappers, such as `EncodeValueRef`, which is commonly used and is completely transparent +fn strip_transparent_types(mut type_name: &str) -> Cow<'_, str> { + let prefixes = [ + "EncodeValueRef<", + // "ApplicationRef<", + // "ContextSpecificRef<", + // "PrivateRef<", + ]; + + for prefix in prefixes { + type_name = if let Some(stripped) = type_name.strip_prefix(prefix) { + stripped.strip_suffix(">").unwrap_or(stripped) + } else { + type_name + }; + } + + Cow::Borrowed(type_name) +} diff --git a/der/src/writer/clarify/commentwriter.rs b/der/src/writer/clarify/commentwriter.rs new file mode 100644 index 000000000..a422ca613 --- /dev/null +++ b/der/src/writer/clarify/commentwriter.rs @@ -0,0 +1,89 @@ +use std::{io::Write, vec::Vec}; + +pub trait CommentWriter { + fn comment(&mut self, s: &str); + + fn before_new_line(&mut self, w: &mut dyn Write); + + fn start_new_line(&mut self, _w: &mut dyn Write) {} + + fn needs_newline_space(&self) -> bool { + false + } +} + +#[derive(Default)] +pub struct JavaCommentWriter { + pub buf: Vec, +} + +impl CommentWriter for JavaCommentWriter { + fn comment(&mut self, s: &str) { + self.buf.extend_from_slice(s.as_bytes()); + } + + fn before_new_line(&mut self, w: &mut dyn Write) { + if self.buf.is_empty() { + return; + } + let _ = w.write_all(b" // "); + let _ = w.write_all(&self.buf); + self.buf.clear(); + } +} + +#[derive(Default)] +pub struct XmlCommentWriter { + pub buf: Vec, +} + +impl CommentWriter for XmlCommentWriter { + fn comment(&mut self, s: &str) { + self.buf.extend_from_slice(s.as_bytes()); + } + + fn before_new_line(&mut self, w: &mut dyn Write) { + if self.buf.is_empty() { + return; + } + let _ = w.write_all(b" "); + self.buf.clear(); + } +} + +#[derive(Default)] +pub struct RustHexWriter { + pub started_newline: bool, + pub buf: Vec, +} + +impl CommentWriter for RustHexWriter { + fn comment(&mut self, s: &str) { + self.buf.extend_from_slice(s.as_bytes()); + } + + fn before_new_line(&mut self, w: &mut dyn Write) { + if self.started_newline { + w.write_all(b"\"").ok(); + } + if self.buf.is_empty() { + return; + } + w.write_all(b" // ").ok(); + w.write_all(&self.buf).ok(); + self.buf.clear(); + self.started_newline = false; + } + + fn start_new_line(&mut self, w: &mut dyn Write) { + self.started_newline = true; + w.write_all(b"\"").ok(); + } + + fn needs_newline_space(&self) -> bool { + // because '"' is in first line, next lines need to be moved by 1 character + true + } +} diff --git a/der/src/writer/clarify/hexdisplaylines.rs b/der/src/writer/clarify/hexdisplaylines.rs new file mode 100644 index 000000000..ea76b6218 --- /dev/null +++ b/der/src/writer/clarify/hexdisplaylines.rs @@ -0,0 +1,29 @@ +use core::fmt::Write; +use std::fmt; + +/// (bytes, indent, nl_ident) +pub struct HexDisplayLines<'a, 'i> { + pub bytes: &'a [u8], + pub indent: &'i str, + pub space: bool, +} + +impl fmt::Display for HexDisplayLines<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut first = true; + for chunk in self.bytes.chunks(16) { + if !first { + write!(f, "\n{}", self.indent)?; + if self.space { + f.write_char(' ').ok(); + } + } else { + first = false; + } + for byte in chunk { + write!(f, "{byte:02X} ")?; + } + } + Ok(()) + } +} diff --git a/der/src/writer/slice.rs b/der/src/writer/slice.rs index 43dfcf1bb..acf6d8104 100644 --- a/der/src/writer/slice.rs +++ b/der/src/writer/slice.rs @@ -106,7 +106,7 @@ impl<'a> SliceWriter<'a> { /// Reserve a portion of the internal buffer, updating the internal cursor /// position and returning a mutable slice. - fn reserve(&mut self, len: impl TryInto) -> Result<&mut [u8]> { + pub(crate) fn reserve(&mut self, len: impl TryInto) -> Result<&mut [u8]> { if self.is_failed() { return Err(ErrorKind::Failed.at(self.position)); } @@ -124,6 +124,11 @@ impl<'a> SliceWriter<'a> { self.position = end; Ok(slice) } + + /// Returns current position in the buffer. + pub fn position(&self) -> Length { + self.position + } } impl Writer for SliceWriter<'_> { diff --git a/der/tests/clarify.rs b/der/tests/clarify.rs new file mode 100644 index 000000000..3ef58d20b --- /dev/null +++ b/der/tests/clarify.rs @@ -0,0 +1,153 @@ +//! Tests for clarify pretty-printing support. +#![cfg(all(feature = "derive", feature = "alloc", feature = "clarify"))] +// TODO: fix needless_question_mark in the derive crate +#![allow(clippy::needless_question_mark)] + +pub mod sequence { + use std::{println, str::FromStr}; + + use const_oid::ObjectIdentifier; + use der::{ + AnyRef, ClarifyFlavor, Decode, EncodeClarifyExt, Sequence, TagNumber, ValueOrd, + asn1::{OctetString, SetOf}, + }; + use hex_literal::hex; + + /// X.509 `AlgorithmIdentifier` + #[derive(Copy, Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)] + pub struct AlgorithmIdentifier<'a> { + pub algorithm: ObjectIdentifier, + pub parameters: Option>, + } + + /// PKCS#8v2 `OneAsymmetricKey` + #[derive(Sequence)] + pub struct OneAsymmetricKey<'a> { + pub version: u8, + pub private_key_algorithm: AlgorithmIdentifier<'a>, + #[asn1(type = "OCTET STRING")] + pub private_key: &'a [u8], + #[asn1(context_specific = "0", extensible = "true", optional = "true")] + pub attributes: Option, 1>>, + #[asn1( + context_specific = "1", + extensible = "true", + optional = "true", + type = "BIT STRING" + )] + pub public_key: Option<&'a [u8]>, + } + + #[test] + fn clarify_simple_octetstring_javacomments() { + let obj = OctetString::new(hex!("AA BB CC")).unwrap(); + + let clarified = obj + .to_der_clarify(ClarifyFlavor::JavaComments) + .expect("encoded DER"); + + assert_eq!( + clarified, + "\n04 03 // tag: OCTET STRING type: OctetString \n\tAA BB CC" + ) + } + + #[test] + fn clarify_simple_octetstring_rusthex() { + let obj = OctetString::new(hex!("AA BB CC")).unwrap(); + + let clarified = obj + .to_der_clarify(ClarifyFlavor::RustHex) + .expect("encoded DER"); + + assert_eq!( + clarified, + "\n\"04 03\" // tag: OCTET STRING type: OctetString \n\t\"AA BB CC\"" + ) + } + + #[test] + fn clarify_simple_octetstring_long_rusthex() { + let obj = OctetString::from_der(&hex!( + "04 11" // tag: OCTET STRING len: 17 type: OctetString + "00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF + 01" + "" // end: OctetString + )) + .unwrap(); + + let clarified = obj + .to_der_clarify(ClarifyFlavor::RustHex) + .expect("encoded DER"); + + println!("clarified: {clarified}"); + assert_eq!( + clarified, + "\n\"04 11\" // tag: OCTET STRING len: 17 type: OctetString \n\t\"00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF \n\t 01\"\n\"\" // end: OctetString " + ); + } + #[test] + fn clarify_one_asymmetric_key_rusthex() { + let obj = OneAsymmetricKey { + version: 1, + private_key_algorithm: AlgorithmIdentifier { + algorithm: ObjectIdentifier::from_str("1.2.3.4.5.6.7.8").expect("valid oid"), + parameters: Some( + AnyRef::new( + der::Tag::ContextSpecific { + constructed: true, + number: TagNumber(0), + }, + &[0xAA, 0xBB], + ) + .expect("valid length"), + ), + }, + private_key: &[ + 0x00, 0x11, 0x22, 0x33, 0x00, 0x11, 0x22, 0x33, 0x00, 0x11, 0x22, 0x33, 0x00, 0x11, + 0x22, 0x33, 0x00, 0x11, 0x22, 0x33, 0x00, 0x11, 0x22, 0x33, 0x00, 0x11, 0x22, 0x33, + ], + attributes: None, + public_key: Some(&[ + 0x44, 0x55, 0x66, 0x77, 0x44, 0x55, 0x66, 0x77, 0x44, 0x55, 0x66, 0x77, 0x44, 0x55, + 0x66, 0x77, 0x44, 0x55, 0x66, 0x77, 0x44, 0x55, 0x66, 0x77, 0x44, 0x55, 0x66, 0x77, + ]), + }; + let clarified = obj + .to_der_clarify(ClarifyFlavor::RustHex) + .expect("encoded DER"); + + //println!("clarified: {clarified}"); + + assert!(clarified.contains("type: OneAsymmetricKey")); + assert!(clarified.contains("tag: CONTEXT-SPECIFIC [0] (constructed)")); + assert!(clarified.contains("tag: CONTEXT-SPECIFIC [1] (constructed)")); + assert!(clarified.contains("type: AlgorithmIdentifier")); + assert!(clarified.contains("tag: OBJECT IDENTIFIER")); + assert!(clarified.contains("type: ObjectIdentifier")); + assert!(clarified.contains("end: OneAsymmetricKey")); + + hex!( + "30 51" // tag: SEQUENCE len: 81 type: OneAsymmetricKey + "02 01" // tag: INTEGER type: u8 + "01" + "30 0D" // tag: SEQUENCE len: 13 type: AlgorithmIdentifier + "06 07" // tag: OBJECT IDENTIFIER type: ObjectIdentifier + "2A 03 04 05 06 07 08" + "A0 02" // tag: CONTEXT-SPECIFIC [0] (constructed) type: AnyRef + "AA BB" + "04 1C" // tag: OCTET STRING len: 28 type: OctetStringRef + "00 11 22 33 00 11 22 33 00 11 22 33 00 11 22 33 + 00 11 22 33 00 11 22 33 00 11 22 33" + "" // end: OctetStringRef + "A1 1F" // tag: CONTEXT-SPECIFIC [1] (constructed) len: 31 type: ContextSpecificRef + "03 1D" // tag: BIT STRING len: 29 type: BitStringRef + "00" + "44 55 66 77 44 55 66 77 44 55 66 77 44 55 66 77 + 44 55 66 77 44 55 66 77 44 55 66 77" + "" // end: BitStringRef + "" // end: ContextSpecificRef + "" // end: OneAsymmetricKey + ); + } +}