Skip to content

Commit e37fa65

Browse files
DarkaMaulwoodruffw
andauthored
Add GeneralizedTime (#492)
* Add GeneralizedTimeFractional Signed-off-by: Alexis <alexis.challande@trailofbits.com> * Fix linting Signed-off-by: Alexis <alexis.challande@trailofbits.com> * Apply suggestions from code review Signed-off-by: Alexis <alexis.challande@trailofbits.com> * Add missing import Signed-off-by: Alexis <alexis.challande@trailofbits.com> * Exercise precision too high Signed-off-by: Alexis <alexis.challande@trailofbits.com> * Correctly handle fractional values Signed-off-by: Alexis <alexis.challande@trailofbits.com> * Use more idiomatic Rust Signed-off-by: Alexis <alexis.challande@trailofbits.com> * Lint * Add CHANGELOG entry * README: rephrase change Signed-off-by: William Woodruff <william@trailofbits.com> * types: remove old comment Signed-off-by: William Woodruff <william@trailofbits.com> * use itoa for formatting Keeps us in a stack buf. Signed-off-by: William Woodruff <william@trailofbits.com> * make clippy happy Signed-off-by: William Woodruff <william@trailofbits.com> * Update src/types.rs --------- Signed-off-by: Alexis <alexis.challande@trailofbits.com> Signed-off-by: William Woodruff <william@trailofbits.com> Co-authored-by: William Woodruff <william@trailofbits.com> Co-authored-by: William Woodruff <william@yossarian.net>
1 parent 841395b commit e37fa65

File tree

6 files changed

+278
-16
lines changed

6 files changed

+278
-16
lines changed

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ std = []
1616

1717
[dependencies]
1818
asn1_derive = { path = "asn1_derive/", version = "0.18.0" }
19+
itoa = "1.0.11"
1920

2021
[dev-dependencies]
2122
libc = "0.2.11"

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ asn1 = { version = "0.18", default-features = false }
3333
rule, it is specific to X.509.
3434
([#494](https://github.com/alex/rust-asn1/pull/494))
3535

36+
- `GeneralizedTime` is a new type that accepts fractional seconds
37+
replacing the old `GeneralizedTime`.
38+
([#492](https://github.com/alex/rust-asn1/pull/492))
39+
3640
[deps-rs-image]: https://deps.rs/repo/github/alex/rust-asn1/status.svg
3741
[deps-rs-link]: https://deps.rs/repo/github/alex/rust-asn1
3842
[docs-rs-image]: https://docs.rs/asn1/badge.svg

src/lib.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -157,11 +157,11 @@ pub use crate::parser::{
157157
pub use crate::tag::Tag;
158158
pub use crate::types::{
159159
Asn1DefinedByReadable, Asn1DefinedByWritable, Asn1Readable, Asn1Writable, BMPString, BigInt,
160-
BigUint, Choice1, Choice2, Choice3, DateTime, DefinedByMarker, Enumerated, Explicit, IA5String,
161-
Implicit, Null, OctetStringEncoded, OwnedBigInt, OwnedBigUint, PrintableString, Sequence,
162-
SequenceOf, SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter, SimpleAsn1Readable,
163-
SimpleAsn1Writable, Tlv, UniversalString, UtcTime, Utf8String, VisibleString,
164-
X509GeneralizedTime,
160+
BigUint, Choice1, Choice2, Choice3, DateTime, DefinedByMarker, Enumerated, Explicit,
161+
GeneralizedTime, IA5String, Implicit, Null, OctetStringEncoded, OwnedBigInt, OwnedBigUint,
162+
PrintableString, Sequence, SequenceOf, SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter,
163+
SimpleAsn1Readable, SimpleAsn1Writable, Tlv, UniversalString, UtcTime, Utf8String,
164+
VisibleString, X509GeneralizedTime,
165165
};
166166
pub use crate::writer::{write, write_single, WriteBuf, WriteError, WriteResult, Writer};
167167

src/parser.rs

+56-4
Original file line numberDiff line numberDiff line change
@@ -347,10 +347,10 @@ mod tests {
347347
use crate::types::Asn1Readable;
348348
use crate::{
349349
BMPString, BigInt, BigUint, BitString, Choice1, Choice2, Choice3, DateTime, Enumerated,
350-
Explicit, IA5String, Implicit, ObjectIdentifier, OctetStringEncoded, OwnedBigInt,
351-
OwnedBigUint, OwnedBitString, ParseError, ParseErrorKind, ParseLocation, ParseResult,
352-
PrintableString, Sequence, SequenceOf, SetOf, Tag, Tlv, UniversalString, UtcTime,
353-
Utf8String, VisibleString, X509GeneralizedTime,
350+
Explicit, GeneralizedTime, IA5String, Implicit, ObjectIdentifier, OctetStringEncoded,
351+
OwnedBigInt, OwnedBigUint, OwnedBitString, ParseError, ParseErrorKind, ParseLocation,
352+
ParseResult, PrintableString, Sequence, SequenceOf, SetOf, Tag, Tlv, UniversalString,
353+
UtcTime, Utf8String, VisibleString, X509GeneralizedTime,
354354
};
355355
#[cfg(not(feature = "std"))]
356356
use alloc::boxed::Box;
@@ -1588,6 +1588,58 @@ mod tests {
15881588
]);
15891589
}
15901590

1591+
#[test]
1592+
fn test_generalized_time() {
1593+
assert_parses::<GeneralizedTime>(&[
1594+
(
1595+
// General case
1596+
Ok(GeneralizedTime::new(
1597+
DateTime::new(2010, 1, 2, 3, 4, 5).unwrap(),
1598+
Some(123_456_000),
1599+
)
1600+
.unwrap()),
1601+
b"\x18\x1620100102030405.123456Z",
1602+
),
1603+
(
1604+
// No fractional time
1605+
Ok(
1606+
GeneralizedTime::new(DateTime::new(2010, 1, 2, 3, 4, 5).unwrap(), None)
1607+
.unwrap(),
1608+
),
1609+
b"\x18\x0f20100102030405Z",
1610+
),
1611+
(
1612+
// Starting with 0 is ok
1613+
Ok(GeneralizedTime::new(
1614+
DateTime::new(2010, 1, 2, 3, 4, 5).unwrap(),
1615+
Some(12_375_600),
1616+
)
1617+
.unwrap()),
1618+
b"\x18\x1720100102030405.0123756Z",
1619+
),
1620+
(
1621+
// But ending with 0 is not OK
1622+
Err(ParseError::new(ParseErrorKind::InvalidValue)),
1623+
b"\x18\x1220100102030405.10Z",
1624+
),
1625+
(
1626+
// Too many digits
1627+
Err(ParseError::new(ParseErrorKind::InvalidValue)),
1628+
b"\x18\x1a20100102030405.0123456789Z",
1629+
),
1630+
(
1631+
// Missing timezone
1632+
Err(ParseError::new(ParseErrorKind::InvalidValue)),
1633+
b"\x18\x1520100102030405.123456",
1634+
),
1635+
(
1636+
// Invalid fractional second
1637+
Err(ParseError::new(ParseErrorKind::InvalidValue)),
1638+
b"\x18\x1020100102030405.Z",
1639+
),
1640+
])
1641+
}
1642+
15911643
#[test]
15921644
fn test_enumerated() {
15931645
assert_parses::<Enumerated>(&[

src/types.rs

+172-3
Original file line numberDiff line numberDiff line change
@@ -914,7 +914,8 @@ fn push_four_digits(dest: &mut WriteBuf, val: u16) -> WriteResult {
914914
}
915915

916916
/// A structure representing a (UTC timezone) date and time.
917-
/// Wrapped by `UtcTime` and `X509GeneralizedTime`.
917+
/// Wrapped by `UtcTime` and `X509GeneralizedTime` and used in
918+
/// `GeneralizedTime`.
918919
#[derive(Debug, Clone, PartialEq, Hash, Eq, PartialOrd)]
919920
pub struct DateTime {
920921
year: u16,
@@ -1088,6 +1089,125 @@ impl SimpleAsn1Writable for X509GeneralizedTime {
10881089
}
10891090
}
10901091

1092+
/// Used for parsing and writing ASN.1 `GENERALIZED TIME` values,
1093+
/// including values with fractional seconds of up to nanosecond
1094+
/// precision.
1095+
#[derive(Debug, Clone, PartialEq, PartialOrd, Hash, Eq)]
1096+
pub struct GeneralizedTime {
1097+
datetime: DateTime,
1098+
nanoseconds: Option<u32>,
1099+
}
1100+
1101+
impl GeneralizedTime {
1102+
pub fn new(dt: DateTime, nanoseconds: Option<u32>) -> ParseResult<GeneralizedTime> {
1103+
if let Some(val) = nanoseconds {
1104+
if val < 1 || val >= 1e9 as u32 {
1105+
return Err(ParseError::new(ParseErrorKind::InvalidValue));
1106+
}
1107+
}
1108+
1109+
Ok(GeneralizedTime {
1110+
datetime: dt,
1111+
nanoseconds,
1112+
})
1113+
}
1114+
1115+
pub fn as_datetime(&self) -> &DateTime {
1116+
&self.datetime
1117+
}
1118+
1119+
pub fn nanoseconds(&self) -> Option<u32> {
1120+
self.nanoseconds
1121+
}
1122+
}
1123+
1124+
fn read_fractional_time(data: &mut &[u8]) -> ParseResult<Option<u32>> {
1125+
// We cannot use read_byte here because it will advance the pointer
1126+
// However, we know that the is suffixed by 'Z' so reading into an empty
1127+
// data should lead to an error.
1128+
if data.first() == Some(&b'.') {
1129+
*data = &data[1..];
1130+
1131+
let mut fraction = 0u32;
1132+
let mut digits = 0;
1133+
// Read up to 9 digits
1134+
for b in data.iter().take(9) {
1135+
if !b.is_ascii_digit() {
1136+
if digits == 0 {
1137+
// We must have at least one digit
1138+
return Err(ParseError::new(ParseErrorKind::InvalidValue));
1139+
}
1140+
break;
1141+
}
1142+
fraction = fraction * 10 + (b - b'0') as u32;
1143+
digits += 1;
1144+
}
1145+
*data = &data[digits..];
1146+
1147+
// No trailing zero
1148+
if fraction % 10 == 0 {
1149+
return Err(ParseError::new(ParseErrorKind::InvalidValue));
1150+
}
1151+
1152+
// Now let scale up in nanoseconds
1153+
let nanoseconds: u32 = fraction * 10u32.pow(9 - digits as u32);
1154+
Ok(Some(nanoseconds))
1155+
} else {
1156+
Ok(None)
1157+
}
1158+
}
1159+
1160+
impl SimpleAsn1Readable<'_> for GeneralizedTime {
1161+
const TAG: Tag = Tag::primitive(0x18);
1162+
fn parse_data(mut data: &[u8]) -> ParseResult<GeneralizedTime> {
1163+
let year = read_4_digits(&mut data)?;
1164+
let month = read_2_digits(&mut data)?;
1165+
let day = read_2_digits(&mut data)?;
1166+
let hour = read_2_digits(&mut data)?;
1167+
let minute = read_2_digits(&mut data)?;
1168+
let second = read_2_digits(&mut data)?;
1169+
1170+
let fraction = read_fractional_time(&mut data)?;
1171+
read_tz_and_finish(&mut data)?;
1172+
1173+
GeneralizedTime::new(
1174+
DateTime::new(year, month, day, hour, minute, second)?,
1175+
fraction,
1176+
)
1177+
}
1178+
}
1179+
1180+
impl SimpleAsn1Writable for GeneralizedTime {
1181+
const TAG: Tag = Tag::primitive(0x18);
1182+
fn write_data(&self, dest: &mut WriteBuf) -> WriteResult {
1183+
let dt = self.as_datetime();
1184+
push_four_digits(dest, dt.year())?;
1185+
push_two_digits(dest, dt.month())?;
1186+
push_two_digits(dest, dt.day())?;
1187+
1188+
push_two_digits(dest, dt.hour())?;
1189+
push_two_digits(dest, dt.minute())?;
1190+
push_two_digits(dest, dt.second())?;
1191+
1192+
if let Some(nanoseconds) = self.nanoseconds() {
1193+
dest.push_byte(b'.')?;
1194+
1195+
let mut buf = itoa::Buffer::new();
1196+
let nanos = buf.format(nanoseconds);
1197+
let pad = 9 - nanos.len();
1198+
let nanos = nanos.trim_end_matches('0');
1199+
1200+
for _ in 0..pad {
1201+
dest.push_byte(b'0')?;
1202+
}
1203+
1204+
dest.push_slice(nanos.as_bytes())?;
1205+
}
1206+
1207+
dest.push_byte(b'Z')
1208+
}
1209+
}
1210+
10911211
/// An ASN.1 `ENUMERATED` value.
10921212
#[derive(Debug, PartialEq, Eq)]
10931213
pub struct Enumerated(u32);
@@ -1725,8 +1845,8 @@ impl<T> DefinedByMarker<T> {
17251845
#[cfg(test)]
17261846
mod tests {
17271847
use crate::{
1728-
parse_single, BigInt, BigUint, DateTime, DefinedByMarker, Enumerated, IA5String,
1729-
ObjectIdentifier, OctetStringEncoded, OwnedBigInt, OwnedBigUint, ParseError,
1848+
parse_single, BigInt, BigUint, DateTime, DefinedByMarker, Enumerated, GeneralizedTime,
1849+
IA5String, ObjectIdentifier, OctetStringEncoded, OwnedBigInt, OwnedBigUint, ParseError,
17301850
ParseErrorKind, PrintableString, SequenceOf, SequenceOfWriter, SetOf, SetOfWriter, Tag,
17311851
Tlv, UtcTime, Utf8String, VisibleString, X509GeneralizedTime,
17321852
};
@@ -2005,6 +2125,55 @@ mod tests {
20052125
assert!(X509GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap()).is_ok());
20062126
}
20072127

2128+
#[test]
2129+
fn test_generalized_time_new() {
2130+
assert!(
2131+
GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), Some(1234))
2132+
.is_ok()
2133+
);
2134+
assert!(
2135+
GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), None).is_ok()
2136+
);
2137+
// Maximum fractional time is 999,999,999 nanos.
2138+
assert!(GeneralizedTime::new(
2139+
DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(),
2140+
Some(999_999_999_u32)
2141+
)
2142+
.is_ok());
2143+
assert!(GeneralizedTime::new(
2144+
DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(),
2145+
Some(1e9 as u32)
2146+
)
2147+
.is_err());
2148+
assert!(GeneralizedTime::new(
2149+
DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(),
2150+
Some(1e9 as u32 + 1)
2151+
)
2152+
.is_err());
2153+
}
2154+
2155+
#[test]
2156+
fn test_generalized_time_partial_ord() {
2157+
let point =
2158+
GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), Some(1234))
2159+
.unwrap();
2160+
assert!(
2161+
point
2162+
< GeneralizedTime::new(DateTime::new(2023, 6, 30, 23, 59, 59).unwrap(), Some(1234))
2163+
.unwrap()
2164+
);
2165+
assert!(
2166+
point
2167+
< GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), Some(1235))
2168+
.unwrap()
2169+
);
2170+
assert!(
2171+
point
2172+
> GeneralizedTime::new(DateTime::new(2015, 6, 30, 23, 59, 59).unwrap(), None)
2173+
.unwrap()
2174+
);
2175+
}
2176+
20082177
#[test]
20092178
fn test_enumerated_value() {
20102179
assert_eq!(Enumerated::new(4).value(), 4);

src/writer.rs

+40-4
Original file line numberDiff line numberDiff line change
@@ -216,10 +216,10 @@ mod tests {
216216
use crate::types::Asn1Writable;
217217
use crate::{
218218
parse_single, BMPString, BigInt, BigUint, BitString, Choice1, Choice2, Choice3, DateTime,
219-
Enumerated, Explicit, IA5String, Implicit, ObjectIdentifier, OctetStringEncoded,
220-
OwnedBigInt, OwnedBigUint, OwnedBitString, PrintableString, Sequence, SequenceOf,
221-
SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter, Tlv, UniversalString, UtcTime,
222-
Utf8String, VisibleString, WriteError, X509GeneralizedTime,
219+
Enumerated, Explicit, GeneralizedTime, IA5String, Implicit, ObjectIdentifier,
220+
OctetStringEncoded, OwnedBigInt, OwnedBigUint, OwnedBitString, PrintableString, Sequence,
221+
SequenceOf, SequenceOfWriter, SequenceWriter, SetOf, SetOfWriter, Tlv, UniversalString,
222+
UtcTime, Utf8String, VisibleString, WriteError, X509GeneralizedTime,
223223
};
224224
#[cfg(not(feature = "std"))]
225225
use alloc::vec::Vec;
@@ -570,6 +570,42 @@ mod tests {
570570
]);
571571
}
572572

573+
#[test]
574+
fn test_write_generalizedtime() {
575+
assert_writes(&[
576+
(
577+
GeneralizedTime::new(DateTime::new(1991, 5, 6, 23, 45, 40).unwrap(), Some(1_234))
578+
.unwrap(),
579+
b"\x18\x1919910506234540.000001234Z",
580+
),
581+
(
582+
GeneralizedTime::new(DateTime::new(1991, 5, 6, 23, 45, 40).unwrap(), Some(1))
583+
.unwrap(),
584+
b"\x18\x1919910506234540.000000001Z",
585+
),
586+
(
587+
GeneralizedTime::new(DateTime::new(1970, 1, 1, 0, 0, 0).unwrap(), None).unwrap(),
588+
b"\x18\x0f19700101000000Z",
589+
),
590+
(
591+
GeneralizedTime::new(
592+
DateTime::new(2009, 11, 15, 22, 56, 16).unwrap(),
593+
Some(100_000_000),
594+
)
595+
.unwrap(),
596+
b"\x18\x1120091115225616.1Z",
597+
),
598+
(
599+
GeneralizedTime::new(
600+
DateTime::new(2009, 11, 15, 22, 56, 16).unwrap(),
601+
Some(999_999_999),
602+
)
603+
.unwrap(),
604+
b"\x18\x1920091115225616.999999999Z",
605+
),
606+
]);
607+
}
608+
573609
#[test]
574610
fn test_write_enumerated() {
575611
assert_writes::<Enumerated>(&[

0 commit comments

Comments
 (0)