Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GeneralizedTime #492

Merged
merged 15 commits into from
Nov 3, 2024
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
60 changes: 56 additions & 4 deletions src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1588,6 +1588,58 @@ mod tests {
]);
}

#[test]
fn test_generalized_time() {
assert_parses::<GeneralizedTime>(&[
(
// 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::<Enumerated>(&[
Expand Down
175 changes: 172 additions & 3 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should DateTime just have an optional nanoseconds field?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, missed this earlier. I don't have a super strong opinion -- I think @DarkaMaul put the nanos on GeneralizedTime since UTCTime has no fractional time support at all, so having it on the top-level DateTime used by both seems (slightly) off.

OTOH having it on GeneralizedTime also seems slightly off, since it's now (DateTime, nanos). So I'm happy to move if you'd prefer.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eh, let's get the docs right on the generalized tiem strcut and that'll be good enough. (See my other comment)

year: u16,
Expand Down Expand Up @@ -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<u32>,
}

impl GeneralizedTime {
pub fn new(dt: DateTime, nanoseconds: Option<u32>) -> ParseResult<GeneralizedTime> {
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<u32> {
self.nanoseconds
}
}

fn read_fractional_time(data: &mut &[u8]) -> ParseResult<Option<u32>> {
// 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<GeneralizedTime> {
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);
Expand Down Expand Up @@ -1725,8 +1845,8 @@ impl<T> DefinedByMarker<T> {
#[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,
};
Expand Down Expand Up @@ -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);
Expand Down
44 changes: 40 additions & 4 deletions src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<Enumerated>(&[
Expand Down