Skip to main content

der/asn1/
utc_time.rs

1//! ASN.1 `UTCTime` support.
2
3use crate::{
4    DecodeValue, EncodeValue, Error, ErrorKind, FixedTag, Header, Length, Reader, Result, Tag,
5    ValueOrd, Writer,
6    datetime::{self, DateTime},
7};
8use core::{cmp::Ordering, time::Duration};
9
10#[cfg(feature = "std")]
11use std::time::SystemTime;
12
13/// ASN.1 `UTCTime` type.
14///
15/// This type implements the validity requirements specified in
16/// [RFC 5280 Section 4.1.2.5.1][1], namely:
17///
18/// > For the purposes of this profile, UTCTime values MUST be expressed in
19/// > Greenwich Mean Time (Zulu) and MUST include seconds (i.e., times are
20/// > `YYMMDDHHMMSSZ`), even where the number of seconds is zero.  Conforming
21/// > systems MUST interpret the year field (`YY`) as follows:
22/// >
23/// > - Where `YY` is greater than or equal to 50, the year SHALL be
24/// >   interpreted as `19YY`; and
25/// > - Where `YY` is less than 50, the year SHALL be interpreted as `20YY`.
26///
27/// Note: Due to common operations working on `UNIX_EPOCH` [`UtcTime`]s are
28/// only supported for the years 1970-2049.
29///
30/// [1]: https://tools.ietf.org/html/rfc5280#section-4.1.2.5.1
31#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
32pub struct UtcTime(DateTime);
33
34impl UtcTime {
35    /// Length of an RFC 5280-flavored ASN.1 DER-encoded [`UtcTime`].
36    pub const LENGTH: usize = 13;
37
38    /// Maximum year that can be represented as a `UTCTime`.
39    pub const MAX_YEAR: u16 = 2049;
40
41    /// Create a [`UtcTime`] from a [`DateTime`].
42    ///
43    /// # Errors
44    /// Returns [`Error`] in the event `datetime` has a year that exceeds [`UtcTime::MAX_YEAR`].
45    pub fn from_date_time(datetime: DateTime) -> Result<Self> {
46        if datetime.year() <= UtcTime::MAX_YEAR {
47            Ok(Self(datetime))
48        } else {
49            Err(Self::TAG.value_error().into())
50        }
51    }
52
53    /// Convert this [`UtcTime`] into a [`DateTime`].
54    #[must_use]
55    pub fn to_date_time(&self) -> DateTime {
56        self.0
57    }
58
59    /// Create a new [`UtcTime`] given a [`Duration`] since `UNIX_EPOCH`
60    /// (a.k.a. "Unix time")
61    ///
62    /// # Errors
63    /// If [`DateTime`] couldn't be created from `unix_duration` successfully.
64    pub fn from_unix_duration(unix_duration: Duration) -> Result<Self> {
65        DateTime::from_unix_duration(unix_duration)?.try_into()
66    }
67
68    /// Get the duration of this timestamp since `UNIX_EPOCH`.
69    #[must_use]
70    pub fn to_unix_duration(&self) -> Duration {
71        self.0.unix_duration()
72    }
73
74    /// Instantiate from [`SystemTime`].
75    ///
76    /// # Errors
77    /// If a time conversion error occurred.
78    #[cfg(feature = "std")]
79    pub fn from_system_time(time: SystemTime) -> Result<Self> {
80        DateTime::try_from(time)
81            .map_err(|_| Self::TAG.value_error())?
82            .try_into()
83    }
84
85    /// Convert to [`SystemTime`].
86    #[cfg(feature = "std")]
87    #[must_use]
88    pub fn to_system_time(&self) -> SystemTime {
89        self.0.to_system_time()
90    }
91}
92
93impl_any_conversions!(UtcTime);
94
95impl<'a> DecodeValue<'a> for UtcTime {
96    type Error = Error;
97
98    fn decode_value<R: Reader<'a>>(reader: &mut R, header: Header) -> Result<Self> {
99        if Self::LENGTH != usize::try_from(header.length())? {
100            return Err(reader.error(Self::TAG.value_error()));
101        }
102
103        let mut bytes = [0u8; Self::LENGTH];
104        reader.read_into(&mut bytes)?;
105
106        match bytes {
107            // RFC 5280 requires mandatory seconds and Z-normalized time zone
108            [
109                year1,
110                year2,
111                mon1,
112                mon2,
113                day1,
114                day2,
115                hour1,
116                hour2,
117                min1,
118                min2,
119                sec1,
120                sec2,
121                b'Z',
122            ] => {
123                let year = u16::from(datetime::decode_decimal(Self::TAG, year1, year2)?);
124                let month = datetime::decode_decimal(Self::TAG, mon1, mon2)?;
125                let day = datetime::decode_decimal(Self::TAG, day1, day2)?;
126                let hour = datetime::decode_decimal(Self::TAG, hour1, hour2)?;
127                let minute = datetime::decode_decimal(Self::TAG, min1, min2)?;
128                let second = datetime::decode_decimal(Self::TAG, sec1, sec2)?;
129
130                // RFC 5280 rules for interpreting the year
131                let year = if year >= 50 {
132                    year.checked_add(1900)
133                } else {
134                    year.checked_add(2000)
135                }
136                .ok_or(ErrorKind::DateTime)?;
137
138                DateTime::new(year, month, day, hour, minute, second)
139                    .map_err(|_| reader.error(Self::TAG.value_error()))
140                    .and_then(|dt| Self::from_unix_duration(dt.unix_duration()))
141            }
142            _ => Err(reader.error(Self::TAG.value_error())),
143        }
144    }
145}
146
147impl EncodeValue for UtcTime {
148    fn value_len(&self) -> Result<Length> {
149        Self::LENGTH.try_into()
150    }
151
152    fn encode_value(&self, writer: &mut impl Writer) -> Result<()> {
153        let year = match self.0.year() {
154            y @ 1950..=1999 => y.checked_sub(1900),
155            y @ 2000..=2049 => y.checked_sub(2000),
156            _ => return Err(Self::TAG.value_error().into()),
157        }
158        .and_then(|y| u8::try_from(y).ok())
159        .ok_or(ErrorKind::DateTime)?;
160
161        datetime::encode_decimal(writer, Self::TAG, year)?;
162        datetime::encode_decimal(writer, Self::TAG, self.0.month())?;
163        datetime::encode_decimal(writer, Self::TAG, self.0.day())?;
164        datetime::encode_decimal(writer, Self::TAG, self.0.hour())?;
165        datetime::encode_decimal(writer, Self::TAG, self.0.minutes())?;
166        datetime::encode_decimal(writer, Self::TAG, self.0.seconds())?;
167        writer.write_byte(b'Z')
168    }
169}
170
171impl FixedTag for UtcTime {
172    const TAG: Tag = Tag::UtcTime;
173}
174
175/// Compares the `YYMMDDHHMMSSZ` encodings (X.690 11.6 orders `SET OF` components by their
176/// encodings), which differs from [`Ord`] (chronological) across 2000: `000101000000Z`
177/// (2000) sorts before `991231235959Z` (1999).
178impl ValueOrd for UtcTime {
179    fn value_cmp(&self, other: &Self) -> Result<Ordering> {
180        // Every field is encoded as two decimal digits, so comparing the fields in order is
181        // comparing the encodings.
182        let fields = |t: &Self| {
183            let dt = &t.0;
184            (
185                dt.year() % 100,
186                dt.month(),
187                dt.day(),
188                dt.hour(),
189                dt.minutes(),
190                dt.seconds(),
191            )
192        };
193        Ok(fields(self).cmp(&fields(other)))
194    }
195}
196
197impl From<&UtcTime> for UtcTime {
198    fn from(value: &UtcTime) -> UtcTime {
199        *value
200    }
201}
202
203impl From<UtcTime> for DateTime {
204    fn from(utc_time: UtcTime) -> DateTime {
205        utc_time.0
206    }
207}
208
209impl From<&UtcTime> for DateTime {
210    fn from(utc_time: &UtcTime) -> DateTime {
211        utc_time.0
212    }
213}
214
215impl TryFrom<DateTime> for UtcTime {
216    type Error = Error;
217
218    fn try_from(datetime: DateTime) -> Result<Self> {
219        Self::from_date_time(datetime)
220    }
221}
222
223impl TryFrom<&DateTime> for UtcTime {
224    type Error = Error;
225
226    fn try_from(datetime: &DateTime) -> Result<Self> {
227        Self::from_date_time(*datetime)
228    }
229}
230
231#[cfg(feature = "std")]
232impl From<UtcTime> for SystemTime {
233    fn from(utc_time: UtcTime) -> SystemTime {
234        utc_time.to_system_time()
235    }
236}
237
238// Implement by hand because the derive would create invalid values.
239// Use the conversion from DateTime to create a valid value.
240// The DateTime type has a way bigger range of valid years than UtcTime,
241// so the DateTime year is mapped into a valid range to throw away less inputs.
242#[cfg(feature = "arbitrary")]
243#[allow(clippy::unwrap_in_result)]
244impl<'a> arbitrary::Arbitrary<'a> for UtcTime {
245    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
246        const MIN_YEAR: u16 = 1970;
247        const VALID_YEAR_COUNT: u16 = UtcTime::MAX_YEAR - MIN_YEAR + 1;
248        const AVERAGE_SECONDS_IN_YEAR: u64 = 31_556_952;
249
250        let datetime = DateTime::arbitrary(u)?;
251        let year = datetime.year();
252        let duration = datetime.unix_duration();
253
254        // Clamp the year into a valid range to not throw away too much input
255        let valid_year = (year.saturating_sub(MIN_YEAR))
256            .rem_euclid(VALID_YEAR_COUNT)
257            .saturating_add(MIN_YEAR);
258        let year_to_remove = year.saturating_sub(valid_year);
259        let valid_duration = duration
260            - Duration::from_secs(
261                u64::from(year_to_remove).saturating_mul(AVERAGE_SECONDS_IN_YEAR),
262            );
263
264        Self::from_date_time(DateTime::from_unix_duration(valid_duration).expect("supported range"))
265            .map_err(|_| arbitrary::Error::IncorrectFormat)
266    }
267
268    fn size_hint(depth: usize) -> (usize, Option<usize>) {
269        DateTime::size_hint(depth)
270    }
271}
272
273#[cfg(test)]
274#[allow(clippy::unwrap_used)]
275mod tests {
276    use super::UtcTime;
277    use crate::{Decode, DerOrd, Encode, SliceWriter, ValueOrd};
278    use core::cmp::Ordering;
279    use hex_literal::hex;
280
281    #[test]
282    fn value_ord_is_encoding_order() {
283        // 1999-12-31 encodes as "99...", 2000-01-01 as "00...": X.690 11.6 puts 2000 first.
284        let y1999 = UtcTime::from_der(&hex!("170d 3939313233313233353935395a")).unwrap();
285        let y2000 = UtcTime::from_der(&hex!("170d 3030303130313030303030305a")).unwrap();
286        assert!(y1999 < y2000); // `Ord` stays chronological
287        assert_eq!(y2000.value_cmp(&y1999), Ok(Ordering::Less));
288        assert_eq!(y2000.der_cmp(&y1999), Ok(Ordering::Less));
289        assert_eq!(y1999.value_cmp(&y1999), Ok(Ordering::Equal));
290    }
291
292    #[test]
293    fn round_trip_vector() {
294        let example_bytes = hex!("17 0d 39 31 30 35 30 36 32 33 34 35 34 30 5a");
295        let utc_time = UtcTime::from_der(&example_bytes).unwrap();
296        assert_eq!(utc_time.to_unix_duration().as_secs(), 673573540);
297
298        let mut buf = [0u8; 128];
299        let mut writer = SliceWriter::new(&mut buf);
300        utc_time.encode(&mut writer).unwrap();
301        assert_eq!(example_bytes, writer.finish().unwrap());
302    }
303}