diff --git a/Cargo.toml b/Cargo.toml index 89a6ce0..10e3993 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ std = [] [dependencies] asn1_derive = { path = "asn1_derive/", version = "0.18.0" } +itoa = "1.0.11" [dev-dependencies] libc = "0.2.11" diff --git a/README.md b/README.md index f167f13..5a7e43a 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,10 @@ asn1 = { version = "0.18", default-features = false } rule, it is specific to X.509. ([#494](https://github.com/alex/rust-asn1/pull/494)) +- `GeneralizedTime` is a new type that accepts fractional seconds + replacing the old `GeneralizedTime`. + ([#492](https://github.com/alex/rust-asn1/pull/492)) + [deps-rs-image]: https://deps.rs/repo/github/alex/rust-asn1/status.svg [deps-rs-link]: https://deps.rs/repo/github/alex/rust-asn1 [docs-rs-image]: https://docs.rs/asn1/badge.svg diff --git a/src/lib.rs b/src/lib.rs index d634b33..d573109 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -157,11 +157,11 @@ pub use crate::parser::{ pub use crate::tag::Tag; pub use crate::types::{ Asn1DefinedByReadable, Asn1DefinedByWritable, Asn1Readable, Asn1Writable, BMPString, BigInt, - BigUint, Choice1, Choice2, Choice3, DateTime, DefinedByMarker, Enumerated, Explicit, IA5String, - Implicit, Null, OctetStringEncoded, OwnedBigInt, OwnedBigUint, PrintableString, Sequence, - SequenceOf, SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter, SimpleAsn1Readable, - SimpleAsn1Writable, Tlv, UniversalString, UtcTime, Utf8String, VisibleString, - X509GeneralizedTime, + BigUint, Choice1, Choice2, Choice3, DateTime, DefinedByMarker, Enumerated, Explicit, + GeneralizedTime, IA5String, Implicit, Null, OctetStringEncoded, OwnedBigInt, OwnedBigUint, + PrintableString, Sequence, SequenceOf, SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter, + SimpleAsn1Readable, SimpleAsn1Writable, Tlv, UniversalString, UtcTime, Utf8String, + VisibleString, X509GeneralizedTime, }; pub use crate::writer::{write, write_single, WriteBuf, WriteError, WriteResult, Writer}; diff --git a/src/parser.rs b/src/parser.rs index b8e3d30..3472c6e 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -347,10 +347,10 @@ mod tests { use crate::types::Asn1Readable; use crate::{ BMPString, BigInt, BigUint, BitString, Choice1, Choice2, Choice3, DateTime, Enumerated, - Explicit, IA5String, Implicit, ObjectIdentifier, OctetStringEncoded, OwnedBigInt, - OwnedBigUint, OwnedBitString, ParseError, ParseErrorKind, ParseLocation, ParseResult, - PrintableString, Sequence, SequenceOf, SetOf, Tag, Tlv, UniversalString, UtcTime, - Utf8String, VisibleString, X509GeneralizedTime, + Explicit, GeneralizedTime, IA5String, Implicit, ObjectIdentifier, OctetStringEncoded, + OwnedBigInt, OwnedBigUint, OwnedBitString, ParseError, ParseErrorKind, ParseLocation, + ParseResult, PrintableString, Sequence, SequenceOf, SetOf, Tag, Tlv, UniversalString, + UtcTime, Utf8String, VisibleString, X509GeneralizedTime, }; #[cfg(not(feature = "std"))] use alloc::boxed::Box; @@ -1588,6 +1588,58 @@ mod tests { ]); } + #[test] + fn test_generalized_time() { + assert_parses::(&[ + ( + // General case + Ok(GeneralizedTime::new( + DateTime::new(2010, 1, 2, 3, 4, 5).unwrap(), + Some(123_456_000), + ) + .unwrap()), + b"\x18\x1620100102030405.123456Z", + ), + ( + // No fractional time + Ok( + GeneralizedTime::new(DateTime::new(2010, 1, 2, 3, 4, 5).unwrap(), None) + .unwrap(), + ), + b"\x18\x0f20100102030405Z", + ), + ( + // Starting with 0 is ok + Ok(GeneralizedTime::new( + DateTime::new(2010, 1, 2, 3, 4, 5).unwrap(), + Some(12_375_600), + ) + .unwrap()), + b"\x18\x1720100102030405.0123756Z", + ), + ( + // But ending with 0 is not OK + Err(ParseError::new(ParseErrorKind::InvalidValue)), + b"\x18\x1220100102030405.10Z", + ), + ( + // Too many digits + Err(ParseError::new(ParseErrorKind::InvalidValue)), + b"\x18\x1a20100102030405.0123456789Z", + ), + ( + // Missing timezone + Err(ParseError::new(ParseErrorKind::InvalidValue)), + b"\x18\x1520100102030405.123456", + ), + ( + // Invalid fractional second + Err(ParseError::new(ParseErrorKind::InvalidValue)), + b"\x18\x1020100102030405.Z", + ), + ]) + } + #[test] fn test_enumerated() { assert_parses::(&[ diff --git a/src/types.rs b/src/types.rs index 55edcb3..f969159 100644 --- a/src/types.rs +++ b/src/types.rs @@ -914,7 +914,8 @@ fn push_four_digits(dest: &mut WriteBuf, val: u16) -> WriteResult { } /// A structure representing a (UTC timezone) date and time. -/// Wrapped by `UtcTime` and `X509GeneralizedTime`. +/// Wrapped by `UtcTime` and `X509GeneralizedTime` and used in +/// `GeneralizedTime`. #[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd)] pub struct DateTime { year: u16, @@ -1088,6 +1089,125 @@ impl SimpleAsn1Writable for X509GeneralizedTime { } } +/// Used for parsing and writing ASN.1 `GENERALIZED TIME` values, +/// including values with fractional seconds of up to nanosecond +/// precision. +#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq)] +pub struct GeneralizedTime { + datetime: DateTime, + nanoseconds: Option, +} + +impl GeneralizedTime { + pub fn new(dt: DateTime, nanoseconds: Option) -> ParseResult { + if let Some(val) = nanoseconds { + if val < 1 || val >= 1e9 as u32 { + return Err(ParseError::new(ParseErrorKind::InvalidValue)); + } + } + + Ok(GeneralizedTime { + datetime: dt, + nanoseconds, + }) + } + + pub fn as_datetime(&self) -> &DateTime { + &self.datetime + } + + pub fn nanoseconds(&self) -> Option { + self.nanoseconds + } +} + +fn read_fractional_time(data: &mut &[u8]) -> ParseResult> { + // We cannot use read_byte here because it will advance the pointer + // However, we know that the is suffixed by 'Z' so reading into an empty + // data should lead to an error. + if data.first() == Some(&b'.') { + *data = &data[1..]; + + let mut fraction = 0u32; + let mut digits = 0; + // Read up to 9 digits + for b in data.iter().take(9) { + if !b.is_ascii_digit() { + if digits == 0 { + // We must have at least one digit + return Err(ParseError::new(ParseErrorKind::InvalidValue)); + } + break; + } + fraction = fraction * 10 + (b - b'0') as u32; + digits += 1; + } + *data = &data[digits..]; + + // No trailing zero + if fraction % 10 == 0 { + return Err(ParseError::new(ParseErrorKind::InvalidValue)); + } + + // Now let scale up in nanoseconds + let nanoseconds: u32 = fraction * 10u32.pow(9 - digits as u32); + Ok(Some(nanoseconds)) + } else { + Ok(None) + } +} + +impl SimpleAsn1Readable<'_> for GeneralizedTime { + const TAG: Tag = Tag::primitive(0x18); + fn parse_data(mut data: &[u8]) -> ParseResult { + let year = read_4_digits(&mut data)?; + let month = read_2_digits(&mut data)?; + let day = read_2_digits(&mut data)?; + let hour = read_2_digits(&mut data)?; + let minute = read_2_digits(&mut data)?; + let second = read_2_digits(&mut data)?; + + let fraction = read_fractional_time(&mut data)?; + read_tz_and_finish(&mut data)?; + + GeneralizedTime::new( + DateTime::new(year, month, day, hour, minute, second)?, + fraction, + ) + } +} + +impl SimpleAsn1Writable for GeneralizedTime { + const TAG: Tag = Tag::primitive(0x18); + fn write_data(&self, dest: &mut WriteBuf) -> WriteResult { + let dt = self.as_datetime(); + push_four_digits(dest, dt.year())?; + push_two_digits(dest, dt.month())?; + push_two_digits(dest, dt.day())?; + + push_two_digits(dest, dt.hour())?; + push_two_digits(dest, dt.minute())?; + push_two_digits(dest, dt.second())?; + + if let Some(nanoseconds) = self.nanoseconds() { + dest.push_byte(b'.')?; + + let mut buf = itoa::Buffer::new(); + let nanos = buf.format(nanoseconds); + let pad = 9 - nanos.len(); + let nanos = nanos.trim_end_matches('0'); + + for _ in 0..pad { + dest.push_byte(b'0')?; + } + + dest.push_slice(nanos.as_bytes())?; + } + + dest.push_byte(b'Z') + } +} + /// An ASN.1 `ENUMERATED` value. #[derive(Debug, PartialEq, Eq)] pub struct Enumerated(u32); @@ -1725,8 +1845,8 @@ impl DefinedByMarker { #[cfg(test)] mod tests { use crate::{ - parse_single, BigInt, BigUint, DateTime, DefinedByMarker, Enumerated, IA5String, - ObjectIdentifier, OctetStringEncoded, OwnedBigInt, OwnedBigUint, ParseError, + parse_single, BigInt, BigUint, DateTime, DefinedByMarker, Enumerated, GeneralizedTime, + IA5String, ObjectIdentifier, OctetStringEncoded, OwnedBigInt, OwnedBigUint, ParseError, ParseErrorKind, PrintableString, SequenceOf, SequenceOfWriter, SetOf, SetOfWriter, Tag, Tlv, UtcTime, Utf8String, VisibleString, X509GeneralizedTime, }; @@ -2005,6 +2125,55 @@ mod tests { assert!(X509GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap()).is_ok()); } + #[test] + fn test_generalized_time_new() { + assert!( + GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), Some(1234)) + .is_ok() + ); + assert!( + GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), None).is_ok() + ); + // Maximum fractional time is 999,999,999 nanos. + assert!(GeneralizedTime::new( + DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), + Some(999_999_999_u32) + ) + .is_ok()); + assert!(GeneralizedTime::new( + DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), + Some(1e9 as u32) + ) + .is_err()); + assert!(GeneralizedTime::new( + DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), + Some(1e9 as u32 + 1) + ) + .is_err()); + } + + #[test] + fn test_generalized_time_partial_ord() { + let point = + GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), Some(1234)) + .unwrap(); + assert!( + point + < GeneralizedTime::new(DateTime::new(2023, 6, 30, 23, 59, 59).unwrap(), Some(1234)) + .unwrap() + ); + assert!( + point + < GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), Some(1235)) + .unwrap() + ); + assert!( + point + > GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), None) + .unwrap() + ); + } + #[test] fn test_enumerated_value() { assert_eq!(Enumerated::new(4).value(), 4); diff --git a/src/writer.rs b/src/writer.rs index 0d27e81..11fcb41 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -216,10 +216,10 @@ mod tests { use crate::types::Asn1Writable; use crate::{ parse_single, BMPString, BigInt, BigUint, BitString, Choice1, Choice2, Choice3, DateTime, - Enumerated, Explicit, IA5String, Implicit, ObjectIdentifier, OctetStringEncoded, - OwnedBigInt, OwnedBigUint, OwnedBitString, PrintableString, Sequence, SequenceOf, - SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter, Tlv, UniversalString, UtcTime, - Utf8String, VisibleString, WriteError, X509GeneralizedTime, + Enumerated, Explicit, GeneralizedTime, IA5String, Implicit, ObjectIdentifier, + OctetStringEncoded, OwnedBigInt, OwnedBigUint, OwnedBitString, PrintableString, Sequence, + SequenceOf, SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter, Tlv, UniversalString, + UtcTime, Utf8String, VisibleString, WriteError, X509GeneralizedTime, }; #[cfg(not(feature = "std"))] use alloc::vec::Vec; @@ -570,6 +570,42 @@ mod tests { ]); } + #[test] + fn test_write_generalizedtime() { + assert_writes(&[ + ( + GeneralizedTime::new(DateTime::new(1991, 5, 6, 23, 45, 40).unwrap(), Some(1_234)) + .unwrap(), + b"\x18\x1919910506234540.000001234Z", + ), + ( + GeneralizedTime::new(DateTime::new(1991, 5, 6, 23, 45, 40).unwrap(), Some(1)) + .unwrap(), + b"\x18\x1919910506234540.000000001Z", + ), + ( + GeneralizedTime::new(DateTime::new(1970, 1, 1, 0, 0, 0).unwrap(), None).unwrap(), + b"\x18\x0f19700101000000Z", + ), + ( + GeneralizedTime::new( + DateTime::new(2009, 11, 15, 22, 56, 16).unwrap(), + Some(100_000_000), + ) + .unwrap(), + b"\x18\x1120091115225616.1Z", + ), + ( + GeneralizedTime::new( + DateTime::new(2009, 11, 15, 22, 56, 16).unwrap(), + Some(999_999_999), + ) + .unwrap(), + b"\x18\x1920091115225616.999999999Z", + ), + ]); + } + #[test] fn test_write_enumerated() { assert_writes::(&[