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