Skip to content

Commit a3c8934

Browse files
authored
Allow TstInfo to read GeneralizedTime with subseconds (#2063)
1 parent be8bde3 commit a3c8934

File tree

2 files changed

+389
-2
lines changed

2 files changed

+389
-2
lines changed
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
use core::str::FromStr;
2+
use core::time::Duration;
3+
use der::{
4+
DateTime, DecodeValue, EncodeValue, ErrorKind, FixedTag, Header, Length, Reader, Result, Tag,
5+
Writer,
6+
};
7+
8+
/// ASN.1 `GeneralizedTime` type.
9+
///
10+
/// This type implements the validity requirements specified in
11+
/// X.690 DER encoding of GeneralizedTime:
12+
///
13+
/// > 11.7.1 - The encoding shall terminate with a "Z"
14+
/// > 11.7.2 - The seconds element shall always be present.
15+
/// > 11.7.3 - The fractional-seconds elements, if present, shall omit all
16+
/// > trailing zeros; if the elements correspond to 0, they shall be wholly
17+
/// > omitted, and the decimal point element also shall be omitted
18+
/// > 11.7.4 - The decimal point element, if present, shall be the point option ".".
19+
/// > 11.7.5 - Midnight (GMT) shall be represented in the form `YYYYMMDD000000Z`
20+
/// > where `YYYYMMDD` represents the day following the midnight in question
21+
#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
22+
pub struct GeneralizedTimeNanos {
23+
datetime: DateTime,
24+
25+
/// Nanoseconds (0-999 999 999)
26+
nanoseconds: u32,
27+
}
28+
29+
impl GeneralizedTimeNanos {
30+
/// Length of an RFC 5280-flavored ASN.1 DER-encoded [`GeneralizedTimeNanos`].
31+
const MIN_LENGTH: usize = 15;
32+
33+
/// Maximum length of a GeneralizedTime containing nanoseconds.
34+
const MAX_LENGTH: usize = Self::MIN_LENGTH + 10;
35+
36+
/// Get the duration of this timestamp since `UNIX_EPOCH`.
37+
pub fn to_unix_duration(&self) -> Duration {
38+
self.datetime.unix_duration() + Duration::from_nanos(u64::from(self.nanoseconds))
39+
}
40+
}
41+
42+
/// Decode 2-digit decimal value
43+
#[allow(clippy::arithmetic_side_effects)]
44+
fn decode_decimal(tag: Tag, hi: u8, lo: u8) -> Result<u8> {
45+
if hi.is_ascii_digit() && lo.is_ascii_digit() {
46+
Ok((hi - b'0') * 10 + (lo - b'0'))
47+
} else {
48+
Err(tag.value_error().into())
49+
}
50+
}
51+
52+
/// Encode 2-digit decimal value
53+
fn encode_decimal<W>(writer: &mut W, tag: Tag, value: u8) -> Result<()>
54+
where
55+
W: Writer + ?Sized,
56+
{
57+
let hi_val = value / 10;
58+
59+
if hi_val >= 10 {
60+
return Err(tag.value_error().into());
61+
}
62+
63+
writer.write_byte(b'0'.checked_add(hi_val).ok_or(ErrorKind::Overflow)?)?;
64+
writer.write_byte(b'0'.checked_add(value % 10).ok_or(ErrorKind::Overflow)?)
65+
}
66+
67+
/// Decode up to 9 digits of fractional seconds, returns them as nanoseconds.
68+
///
69+
/// Assumes DER encoding rules, so no trailing zeroes.
70+
fn decode_fractional_secs(tag: Tag, fract: &[u8]) -> Result<u32> {
71+
// An empty fractional should be checked for by the caller, and we only
72+
// support up to 9 digits (for nanoseconds accuracy).
73+
if fract.is_empty() || fract.len() > 9 {
74+
return Err(tag.value_error().into());
75+
}
76+
77+
// The fractional seconds should not containing trailing zeros according
78+
// to DER encoding rules.
79+
if fract.last() == Some(&b'0') {
80+
return Err(tag.value_error().into());
81+
}
82+
83+
// We're going to use u32::from_str, which accepts a leading + sign.
84+
// So make sure the leading digit is not a + but a proper digit.
85+
if !fract[0].is_ascii_digit() {
86+
return Err(tag.value_error().into());
87+
}
88+
89+
// Decode the number. Use u32::from_ascii when it stabilizes, so we don't
90+
// pay the unnecessary cost of utf8 checking.
91+
let fract_str = core::str::from_utf8(fract).map_err(|_| tag.value_error())?;
92+
let fract_num = u32::from_str(fract_str).map_err(|_| tag.value_error())?;
93+
94+
// Multiply the number to turn it into a nanosecond figure.
95+
let out = fract_num * 10_u32.pow(9 - u32::try_from(fract.len())?);
96+
97+
Ok(out)
98+
}
99+
100+
fn for_each_digits_without_trailing_zeroes<F>(mut nanoseconds: u32, mut f: F) -> Result<()>
101+
where
102+
F: FnMut(u8) -> Result<()>,
103+
{
104+
let mut idx = 100_000_000;
105+
while nanoseconds != 0 && idx != 0 {
106+
let cur_val = u8::try_from((nanoseconds / idx) % 10).map_err(|_| ErrorKind::Overflow)?;
107+
nanoseconds -= u32::from(cur_val) * idx;
108+
idx /= 10;
109+
f(cur_val)?
110+
}
111+
Ok(())
112+
}
113+
114+
/// Encodes the given nanoseconds as fractional secs, discarding trailing
115+
/// zeroes.
116+
fn encode_fractional_secs<W>(writer: &mut W, tag: Tag, nanoseconds: u32) -> Result<()>
117+
where
118+
W: Writer + ?Sized,
119+
{
120+
// We should never have more than 999_999_999 nanoseconds in a second.
121+
if nanoseconds >= 1_000_000_000 {
122+
return Err(tag.value_error().into());
123+
}
124+
125+
// if u32::format_into ever gets stabilized, this can be better written as
126+
//
127+
// ```
128+
// let mut buf = NumBuffer::new();
129+
// let s = value.format_into(&mut buf);
130+
// s.trim_end_matches('0');
131+
// writer.write(s.as_bytes())?;
132+
// ```
133+
//
134+
// This would benefit from using the standard library implementation of
135+
// formatting number, which is much more optimized, using LUTs to avoid a
136+
// lot of the work and handling multiple digits simultaneously.
137+
for_each_digits_without_trailing_zeroes(nanoseconds, |cur_val| {
138+
writer.write_byte(b'0'.checked_add(cur_val).ok_or(ErrorKind::Overflow)?)
139+
})?;
140+
Ok(())
141+
}
142+
143+
/// Creates a [`GeneralizedTimeNanos`] from its individual, ascii
144+
/// encoded components.
145+
#[allow(clippy::too_many_arguments, reason = "Simple helper function")]
146+
fn decode_from_values(
147+
year: (u8, u8, u8, u8),
148+
month: (u8, u8),
149+
day: (u8, u8),
150+
hour: (u8, u8),
151+
min: (u8, u8),
152+
sec: (u8, u8),
153+
fract: Option<&[u8]>,
154+
) -> Result<GeneralizedTimeNanos> {
155+
let year = u16::from(decode_decimal(GeneralizedTimeNanos::TAG, year.0, year.1)?)
156+
.checked_mul(100)
157+
.and_then(|y| {
158+
y.checked_add(
159+
decode_decimal(GeneralizedTimeNanos::TAG, year.2, year.3)
160+
.ok()?
161+
.into(),
162+
)
163+
})
164+
.ok_or(ErrorKind::DateTime)?;
165+
let month = decode_decimal(GeneralizedTimeNanos::TAG, month.0, month.1)?;
166+
let day = decode_decimal(GeneralizedTimeNanos::TAG, day.0, day.1)?;
167+
let hour = decode_decimal(GeneralizedTimeNanos::TAG, hour.0, hour.1)?;
168+
let minute = decode_decimal(GeneralizedTimeNanos::TAG, min.0, min.1)?;
169+
let second = decode_decimal(GeneralizedTimeNanos::TAG, sec.0, sec.1)?;
170+
171+
let nanoseconds = if let Some(fract) = fract {
172+
decode_fractional_secs(GeneralizedTimeNanos::TAG, fract)?
173+
} else {
174+
0
175+
};
176+
177+
let datetime = DateTime::new(year, month, day, hour, minute, second)
178+
.map_err(|_| GeneralizedTimeNanos::TAG.value_error())?;
179+
180+
Ok(GeneralizedTimeNanos {
181+
datetime,
182+
nanoseconds,
183+
})
184+
}
185+
186+
impl<'a> DecodeValue<'a> for GeneralizedTimeNanos {
187+
type Error = der::Error;
188+
189+
#[rustfmt::skip] // Keep the match readable on a single line.
190+
fn decode_value<R: Reader<'a>>(reader: &mut R, header: Header) -> Result<Self> {
191+
let len = usize::try_from(header.length())?;
192+
if !(Self::MIN_LENGTH..=Self::MAX_LENGTH).contains(&len) {
193+
return Err(Self::TAG.value_error().into());
194+
}
195+
196+
let mut bytes = [0u8; Self::MAX_LENGTH];
197+
let data = reader.read_into(&mut bytes[..len])?;
198+
199+
match data {
200+
// No nanoseconds
201+
[y1, y2, y3, y4, mon1, mon2, day1, day2, hour1, hour2, min1, min2, sec1, sec2, b'Z'] =>
202+
decode_from_values((*y1, *y2, *y3, *y4), (*mon1, *mon2), (*day1, *day2),
203+
(*hour1, *hour2), (*min1, *min2), (*sec1, *sec2), None),
204+
// With nanoseconds
205+
[y1, y2, y3, y4, mon1, mon2, day1, day2, hour1, hour2, min1, min2, sec1, sec2, b'.', fract @ .., b'Z'] =>
206+
decode_from_values((*y1, *y2, *y3, *y4), (*mon1, *mon2), (*day1, *day2),
207+
(*hour1, *hour2), (*min1, *min2), (*sec1, *sec2), Some(fract)),
208+
_ => Err(Self::TAG.value_error().into()),
209+
}
210+
}
211+
}
212+
213+
impl EncodeValue for GeneralizedTimeNanos {
214+
fn value_len(&self) -> Result<Length> {
215+
let mut len = Self::MIN_LENGTH;
216+
if self.nanoseconds != 0 {
217+
// Count the number of digits we'll encode.
218+
for_each_digits_without_trailing_zeroes(self.nanoseconds, |_| {
219+
len += 1;
220+
Ok(())
221+
})?;
222+
// + 1 for the decimal separator.
223+
len += 1;
224+
}
225+
Length::try_from(len)
226+
}
227+
228+
fn encode_value(&self, writer: &mut impl Writer) -> Result<()> {
229+
let year_hi = u8::try_from(self.datetime.year() / 100)?;
230+
let year_lo = u8::try_from(self.datetime.year() % 100)?;
231+
232+
encode_decimal(writer, Self::TAG, year_hi)?;
233+
encode_decimal(writer, Self::TAG, year_lo)?;
234+
encode_decimal(writer, Self::TAG, self.datetime.month())?;
235+
encode_decimal(writer, Self::TAG, self.datetime.day())?;
236+
encode_decimal(writer, Self::TAG, self.datetime.hour())?;
237+
encode_decimal(writer, Self::TAG, self.datetime.minutes())?;
238+
encode_decimal(writer, Self::TAG, self.datetime.seconds())?;
239+
if self.nanoseconds != 0 {
240+
writer.write_byte(b'.')?;
241+
encode_fractional_secs(writer, Self::TAG, self.nanoseconds)?;
242+
}
243+
writer.write_byte(b'Z')
244+
}
245+
}
246+
247+
impl FixedTag for GeneralizedTimeNanos {
248+
const TAG: Tag = Tag::GeneralizedTime;
249+
}
250+
251+
#[cfg(test)]
252+
#[allow(clippy::unwrap_used)]
253+
mod tests {
254+
use super::GeneralizedTimeNanos;
255+
use der::{Decode, Encode, SliceWriter};
256+
use hex_literal::hex;
257+
258+
fn round_trip(der: &[u8], expected_timestamp: u64, expected_nanos: u32) {
259+
let utc_time = GeneralizedTimeNanos::from_der(der).unwrap();
260+
assert_eq!(utc_time.to_unix_duration().as_secs(), expected_timestamp);
261+
assert_eq!(utc_time.to_unix_duration().subsec_nanos(), expected_nanos);
262+
263+
let mut buf = [0u8; 128];
264+
let mut encoder = SliceWriter::new(&mut buf);
265+
utc_time.encode(&mut encoder).unwrap();
266+
assert_eq!(der, encoder.finish().unwrap());
267+
}
268+
269+
#[test]
270+
fn round_trip_normal() {
271+
let example_bytes = hex!("18 0f 31 39 39 31 30 35 30 36 32 33 34 35 34 30 5a");
272+
round_trip(&example_bytes, 673573540, 0);
273+
}
274+
275+
#[test]
276+
fn round_trip_nanoseconds_100_000_000() {
277+
let example_bytes = hex!("18 11 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 5A");
278+
round_trip(&example_bytes, 1728312610, 100_000_000);
279+
}
280+
281+
#[test]
282+
fn round_trip_nanoseconds_120_000_000() {
283+
let example_bytes = hex!("18 12 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 5A");
284+
round_trip(&example_bytes, 1728312610, 120_000_000);
285+
}
286+
287+
#[test]
288+
fn round_trip_nanoseconds_123_000_000() {
289+
let example_bytes = hex!("18 13 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 5A");
290+
round_trip(&example_bytes, 1728312610, 123_000_000);
291+
}
292+
293+
#[test]
294+
fn round_trip_nanoseconds_123_400_000() {
295+
let example_bytes =
296+
hex!("18 14 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 5A");
297+
round_trip(&example_bytes, 1728312610, 123_400_000);
298+
}
299+
300+
#[test]
301+
fn round_trip_nanoseconds_123_450_000() {
302+
let example_bytes =
303+
hex!("18 15 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 35 5A");
304+
round_trip(&example_bytes, 1728312610, 123_450_000);
305+
}
306+
307+
#[test]
308+
fn round_trip_nanoseconds_123_456_000() {
309+
let example_bytes =
310+
hex!("18 16 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 35 36 5A");
311+
round_trip(&example_bytes, 1728312610, 123_456_000);
312+
}
313+
314+
#[test]
315+
fn round_trip_nanoseconds_123_456_700() {
316+
let example_bytes =
317+
hex!("18 17 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 35 36 37 5A");
318+
round_trip(&example_bytes, 1728312610, 123_456_700);
319+
}
320+
321+
#[test]
322+
fn round_trip_nanoseconds_123_456_780() {
323+
let example_bytes =
324+
hex!("18 18 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 35 36 37 38 5A");
325+
round_trip(&example_bytes, 1728312610, 123_456_780);
326+
}
327+
328+
#[test]
329+
fn round_trip_nanoseconds_123_456_789() {
330+
let example_bytes = hex!(
331+
"18 19 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 35 36 37 38 39 5A"
332+
);
333+
round_trip(&example_bytes, 1728312610, 123_456_789);
334+
}
335+
336+
#[test]
337+
fn round_trip_nanoseconds_000_000_005() {
338+
let example_bytes = hex!(
339+
"18 19 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 30 30 30 30 30 30 30 30 35 5A"
340+
);
341+
round_trip(&example_bytes, 1728312610, 5);
342+
}
343+
344+
#[test]
345+
fn invalid_generalized_time_delimiter_no_subsec() {
346+
let example_bytes = hex!("18 10 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 5A");
347+
let err = GeneralizedTimeNanos::from_der(&example_bytes).unwrap_err();
348+
assert_eq!(
349+
err.kind(),
350+
der::ErrorKind::Value {
351+
tag: der::Tag::GeneralizedTime
352+
}
353+
);
354+
}
355+
356+
#[test]
357+
fn invalid_generalized_time_trailing_zeroes() {
358+
let example_bytes = hex!(
359+
"18 1A 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 33 32 30 30 30 30 30 30 30 30 5A"
360+
);
361+
let err = GeneralizedTimeNanos::from_der(&example_bytes).unwrap_err();
362+
assert_eq!(
363+
err.kind(),
364+
der::ErrorKind::Value {
365+
tag: der::Tag::GeneralizedTime
366+
}
367+
);
368+
}
369+
370+
#[test]
371+
fn invalid_generalized_time_too_long() {
372+
let example_bytes = hex!(
373+
"18 1A 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 30 30 30 30 30 30 30 30 30 35 5A"
374+
);
375+
let err = GeneralizedTimeNanos::from_der(&example_bytes).unwrap_err();
376+
assert_eq!(
377+
err.kind(),
378+
der::ErrorKind::Value {
379+
tag: der::Tag::GeneralizedTime
380+
}
381+
);
382+
}
383+
}

0 commit comments

Comments
 (0)