From 9dc64bd1d0806624b74eea69e052338cf445260b Mon Sep 17 00:00:00 2001 From: Niklas Saari Date: Mon, 28 Aug 2023 10:43:12 +0300 Subject: [PATCH] ber: Split and improve UTCTime type (#152) --- src/ber.rs | 46 +++++++++++++++++++++++++++- src/ber/de.rs | 82 +++++++++++++++++++++++++++++++++++++++----------- src/ber/enc.rs | 17 ++++++++--- 3 files changed, 122 insertions(+), 23 deletions(-) diff --git a/src/ber.rs b/src/ber.rs index 5fd48e14..b54a4e1d 100644 --- a/src/ber.rs +++ b/src/ber.rs @@ -44,6 +44,7 @@ mod tests { use alloc::borrow::ToOwned; use alloc::vec; use alloc::vec::Vec; + use chrono::{DateTime, FixedOffset, NaiveDate, Utc}; use crate::types::*; @@ -215,7 +216,6 @@ mod tests { } #[test] fn test_generalized_time() { - use chrono::{DateTime, FixedOffset, NaiveDate, Utc}; // "20801009130005.342Z" let offset = chrono::FixedOffset::east_opt(0).unwrap(); let dt = NaiveDate::from_ymd_opt(2080, 10, 9) @@ -300,4 +300,48 @@ mod tests { assert!(result.is_ok()); assert_eq!(dt1, result.unwrap()); } + #[test] + fn test_utc_time() { + // "180122132900Z" + round_trip!( + ber, + UtcTime, + UtcTime::from( + NaiveDate::from_ymd_opt(2018, 1, 22) + .unwrap() + .and_hms_opt(13, 29, 0) + .unwrap() + .and_utc() + ), + &[ + 0x17, 0x0d, 0x31, 0x38, 0x30, 0x31, 0x32, 0x32, 0x31, 0x33, 0x32, 0x39, 0x30, 0x30, + 0x5a + ] + ); + // "230122130000-0500" - converts to canonical form "230122180000Z" + let offset = FixedOffset::east_opt(-3600 * 5).unwrap(); + let dt1 = UtcTime::from(DateTime::::from_local( + NaiveDate::from_ymd_opt(2023, 1, 22) + .unwrap() + .and_hms_opt(13, 0, 0) + .unwrap(), + offset, + )); + round_trip!( + ber, + UtcTime, + dt1, + &[ + 0x17, 0x0d, 0x32, 0x33, 0x30, 0x31, 0x32, 0x32, 0x31, 0x38, 0x30, 0x30, 0x30, 0x30, + 0x5a + ] + ); + // "230122130000-0500" as bytes + let data = [ + 23, 17, 50, 51, 48, 49, 50, 50, 49, 51, 48, 48, 48, 48, 45, 48, 53, 48, 48, + ]; + let result = crate::ber::decode::(&data); + assert!(result.is_ok()); + assert_eq!(dt1, result.unwrap()); + } } diff --git a/src/ber/de.rs b/src/ber/de.rs index b2719357..b63e7ab1 100644 --- a/src/ber/de.rs +++ b/src/ber/de.rs @@ -256,6 +256,57 @@ impl<'input> Decoder<'input> { Err(Error::InvalidDate) }; } + /// Parse any UTCTime string, can be any from ASN.1 definition + /// TODO, move to type itself? + pub fn parse_any_utc_time_string( + string: alloc::string::String, + ) -> Result { + // When compared to GeneralizedTime, UTC time has no fractions. + let len = string.len(); + // Largest string, e.g. "820102070000-0500".len() == 17 + if len > 17 { + return Err(Error::InvalidDate); + } + let format = if string.contains('Z') { + if len == 11 { + "%y%m%d%H%MZ" + } else { + "%y%m%d%H%M%SZ" + } + } else if len == 15 { + "%y%m%d%H%M%z" + } else { + "%y%m%d%H%M%S%z" + }; + match len { + 11 | 13 => { + let naive = NaiveDateTime::parse_from_str(&string, format) + .map_err(|_| Error::InvalidDate)?; + Ok(DateTime::::from_utc(naive, Utc).into()) + } + 15 | 17 => Ok(DateTime::parse_from_str(&string, format) + .map_err(|_| Error::InvalidDate)? + .into()), + _ => Err(Error::InvalidDate), + } + } + + /// Enforce CER/DER restrictions defined in Section 11.8, strictly raise error on non-compliant + pub fn parse_canonical_utc_time_string( + string: alloc::string::String, + ) -> Result { + let len = string.len(); + return if string.ends_with('Z') { + let naive = match len { + 13 => NaiveDateTime::parse_from_str(&string, "%y%m%d%H%M%SZ") + .map_err(|_| Error::InvalidDate)?, + _ => Err(Error::InvalidDate)?, + }; + Ok(DateTime::::from_utc(naive, Utc).into()) + } else { + Err(Error::InvalidDate) + }; + } } impl<'input> crate::Decoder for Decoder<'input> { @@ -481,24 +532,12 @@ impl<'input> crate::Decoder for Decoder<'input> { fn decode_utc_time(&mut self, tag: Tag) -> Result { // Reference https://obj-sys.com/asn1tutorial/node15.html - // FIXME - should this be DateTime rather than NaiveDateTime ? let string = self.decode_utf8_string(tag, <_>::default())?; - let format = if string.contains('Z') { - if string.len() == 11 { - "%y%m%d%H%MZ" - } else { - "%y%m%d%H%M%SZ" - } - } else if string.len() == 15 { - "%y%m%d%H%M%z" + if self.config.encoding_rules.is_ber() { + Self::parse_any_utc_time_string(string) } else { - "%y%m%d%H%M%S%z" - }; - - chrono::NaiveDateTime::parse_from_str(&string, format) - .ok() - .context(error::InvalidDateSnafu) - .map(|date| types::UtcTime::from_utc(date, chrono::Utc)) + Self::parse_canonical_utc_time_string(string) + } } fn decode_sequence_of( @@ -767,22 +806,31 @@ mod tests { let time = crate::types::GeneralizedTime::parse_from_str("991231235959+0000", "%y%m%d%H%M%S%z") .unwrap(); + // 991231235959Z let has_z = &[ 0x17, 0x0D, 0x39, 0x39, 0x31, 0x32, 0x33, 0x31, 0x32, 0x33, 0x35, 0x39, 0x35, 0x39, 0x5A, ]; + // 991231235959+0000 let has_noz = &[ 0x17, 0x11, 0x39, 0x39, 0x31, 0x32, 0x33, 0x31, 0x32, 0x33, 0x35, 0x39, 0x35, 0x39, - 0x2B, 0x30, 0x32, 0x30, 0x30, + 0x2B, 0x30, 0x30, 0x30, 0x30, ]; assert_eq!( time, decode::>(has_z).unwrap() ); + + assert_eq!( + time, + crate::der::decode::(has_z).unwrap() + ); + assert_eq!( time, decode::>(has_noz).unwrap() ); + assert!(crate::der::decode::(has_noz).is_err()); } #[test] diff --git a/src/ber/enc.rs b/src/ber/enc.rs index dce5d25f..2c29ac0e 100644 --- a/src/ber/enc.rs +++ b/src/ber/enc.rs @@ -282,6 +282,17 @@ impl Encoder { string.push('Z'); string.into_bytes() } + + #[must_use] + /// Canonical byte presentation for CER/DER UTCTime as defined in X.690 section 11.8. + /// Also used for BER on this crate. + pub fn datetime_to_canonical_utc_time_bytes(value: &chrono::DateTime) -> Vec { + value + .naive_utc() + .format("%y%m%d%H%M%SZ") + .to_string() + .into_bytes() + } } impl crate::Encoder for Encoder { @@ -456,11 +467,7 @@ impl crate::Encoder for Encoder { ) -> Result { self.encode_primitive( tag, - value - .naive_utc() - .format("%y%m%d%H%M%SZ") - .to_string() - .as_bytes(), + Self::datetime_to_canonical_utc_time_bytes(value).as_slice(), ); Ok(())