x509_certificate/
asn1time.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! ASN.1 primitives related to time types.
6
7use {
8    bcder::{
9        decode::{Constructed, DecodeError, Primitive, SliceSource, Source},
10        encode::{PrimitiveContent, Values},
11        Mode, Tag,
12    },
13    chrono::{Datelike, TimeZone, Timelike},
14    std::{
15        fmt::{Display, Formatter},
16        io::Write,
17        ops::{Add, Deref},
18        str::FromStr,
19    },
20};
21
22/// Timezone formats of [GeneralizedTime] to allow.
23///
24/// X.690 allows [GeneralizedTime] to have multiple timezone formats. However,
25/// various RFCs restrict which formats are allowed. This enumeration exists to
26/// express which formats should be allowed by a parser.
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum GeneralizedTimeAllowedTimezone {
29    /// Allow either `Z` or timezone offset formats.
30    Any,
31
32    /// `Z` timezone identifier only.
33    Z,
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum Time {
38    UtcTime(UtcTime),
39    GeneralTime(GeneralizedTime),
40}
41
42impl Time {
43    pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, DecodeError<S::Error>> {
44        cons.take_primitive(|tag, prim| match tag {
45            Tag::UTC_TIME => Ok(Self::UtcTime(UtcTime::from_primitive(prim)?)),
46            Tag::GENERALIZED_TIME => Ok(Self::GeneralTime(
47                GeneralizedTime::from_primitive_no_fractional_or_timezone_offsets(prim)?,
48            )),
49            _ => Err(prim.content_err(format!("expected UTCTime or GeneralizedTime; got {}", tag))),
50        })
51    }
52
53    pub fn take_opt_from<S: Source>(
54        cons: &mut Constructed<S>,
55    ) -> Result<Option<Self>, DecodeError<S::Error>> {
56        cons.take_opt_primitive(|tag, prim| match tag {
57            Tag::UTC_TIME => Ok(Self::UtcTime(UtcTime::from_primitive(prim)?)),
58            Tag::GENERALIZED_TIME => Ok(Self::GeneralTime(
59                GeneralizedTime::from_primitive_no_fractional_or_timezone_offsets(prim)?,
60            )),
61            _ => Err(prim.content_err(format!("expected UTCTime or GeneralizedTime; got {}", tag))),
62        })
63    }
64
65    pub fn encode_ref(&self) -> impl Values + '_ {
66        match self {
67            Self::UtcTime(utc) => (Some(utc.encode()), None),
68            Self::GeneralTime(gt) => (None, Some(gt.encode())),
69        }
70    }
71}
72
73impl From<chrono::DateTime<chrono::Utc>> for Time {
74    fn from(t: chrono::DateTime<chrono::Utc>) -> Self {
75        Self::UtcTime(UtcTime(t))
76    }
77}
78
79impl From<Time> for chrono::DateTime<chrono::Utc> {
80    fn from(value: Time) -> Self {
81        match value {
82            Time::UtcTime(v) => v.into(),
83            Time::GeneralTime(v) => v.into(),
84        }
85    }
86}
87
88#[derive(Clone, Debug, Eq, PartialEq)]
89enum Zone {
90    Utc,
91    Offset(chrono::FixedOffset),
92}
93
94impl Display for Zone {
95    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::Utc => f.write_str("Z"),
98            Self::Offset(offset) => f.write_str(format!("{}", offset).replace(':', "").as_str()),
99        }
100    }
101}
102
103#[derive(Clone, Debug, Eq, PartialEq)]
104pub struct GeneralizedTime {
105    time: chrono::NaiveDateTime,
106    fractional_seconds: bool,
107    timezone: Zone,
108}
109
110impl GeneralizedTime {
111    /// Take a value, not allowing fractional seconds and requiring the `Z` timezone identifier.
112    pub fn take_from_no_fractional_z<S: Source>(
113        cons: &mut Constructed<S>,
114    ) -> Result<Self, DecodeError<S::Error>> {
115        cons.take_primitive_if(Tag::GENERALIZED_TIME, |prim| {
116            Self::from_primitive_no_fractional_or_timezone_offsets(prim)
117        })
118    }
119
120    /// Take a value, allowing fractional seconds and requiring the `Z` timezone identifier.
121    pub fn take_from_allow_fractional_z<S: Source>(
122        cons: &mut Constructed<S>,
123    ) -> Result<Self, DecodeError<S::Error>> {
124        cons.take_primitive_if(Tag::GENERALIZED_TIME, |prim| {
125            let data = prim.take_all()?;
126
127            Self::parse(
128                SliceSource::new(data.as_ref()),
129                true,
130                GeneralizedTimeAllowedTimezone::Z,
131            )
132            .map_err(|e| e.convert())
133        })
134    }
135
136    /// Parse a [GeneralizedTime] from a primitive string and don't allow fractional seconds or timezone offsets.
137    pub fn from_primitive_no_fractional_or_timezone_offsets<S: Source>(
138        prim: &mut Primitive<S>,
139    ) -> Result<Self, DecodeError<S::Error>> {
140        let data = prim.take_all()?;
141
142        Self::parse(
143            SliceSource::new(data.as_ref()),
144            false,
145            GeneralizedTimeAllowedTimezone::Z,
146        )
147        .map_err(|e| e.convert())
148    }
149
150    /// Parse GeneralizedTime string data.
151    pub fn parse(
152        source: SliceSource,
153        allow_fractional_seconds: bool,
154        tz: GeneralizedTimeAllowedTimezone,
155    ) -> Result<Self, DecodeError<<SliceSource as Source>::Error>> {
156        let data = source.slice();
157
158        // This form is common and is most strict. So check for it quickly.
159        if !allow_fractional_seconds
160            && matches!(tz, GeneralizedTimeAllowedTimezone::Z)
161            && data.len() != "YYYYMMDDHHMMSSZ".len()
162        {
163            return Err(source.content_err(format!(
164                "{} is not of format YYYYMMDDHHMMSSZ",
165                String::from_utf8_lossy(data)
166            )));
167        }
168
169        if data.len() < 15 {
170            return Err(source.content_err("GeneralizedTime value too short"));
171        }
172
173        let year = i32::from_str(
174            std::str::from_utf8(&data[0..4]).map_err(|s| source.content_err(s.to_string()))?,
175        )
176        .map_err(|s| source.content_err(s.to_string()))?;
177        let month = u32::from_str(
178            std::str::from_utf8(&data[4..6]).map_err(|s| source.content_err(s.to_string()))?,
179        )
180        .map_err(|s| source.content_err(s.to_string()))?;
181        let day = u32::from_str(
182            std::str::from_utf8(&data[6..8]).map_err(|s| source.content_err(s.to_string()))?,
183        )
184        .map_err(|s| source.content_err(s.to_string()))?;
185        let hour = u32::from_str(
186            std::str::from_utf8(&data[8..10]).map_err(|s| source.content_err(s.to_string()))?,
187        )
188        .map_err(|s| source.content_err(s.to_string()))?;
189        let minute = u32::from_str(
190            std::str::from_utf8(&data[10..12]).map_err(|s| source.content_err(s.to_string()))?,
191        )
192        .map_err(|s| source.content_err(s.to_string()))?;
193        let second = u32::from_str(
194            std::str::from_utf8(&data[12..14]).map_err(|s| source.content_err(s.to_string()))?,
195        )
196        .map_err(|s| source.content_err(s.to_string()))?;
197
198        let remaining = &data[14..];
199
200        let (nano, remaining) = match allow_fractional_seconds {
201            true => {
202                if remaining.starts_with(b".") {
203                    if let Some((nondigit_offset, _)) = remaining
204                        .iter()
205                        .enumerate()
206                        .skip(1)
207                        .find(|(_, c)| !c.is_ascii_digit())
208                    {
209                        let digits_count = nondigit_offset - 1;
210
211                        let (digits, remaining) = remaining.split_at(nondigit_offset);
212
213                        let mut digits = std::str::from_utf8(&digits[1..])
214                            .map_err(|s| source.content_err(s.to_string()))?
215                            .to_string();
216                        digits.extend(std::iter::repeat('0').take(9 - digits_count));
217
218                        (
219                            u32::from_str(&digits)
220                                .map_err(|s| source.content_err(s.to_string()))?,
221                            remaining,
222                        )
223                    } else {
224                        return Err(source.content_err(
225                            "failed to locate timezone identifier after fractional seconds",
226                        ));
227                    }
228                } else {
229                    (0, remaining)
230                }
231            }
232            false => (0, remaining),
233        };
234
235        let timezone = match tz {
236            GeneralizedTimeAllowedTimezone::Z => {
237                if remaining != b"Z" {
238                    return Err(source.content_err(format!(
239                        "timezone identifier {} not `Z`",
240                        String::from_utf8_lossy(remaining)
241                    )));
242                }
243
244                Zone::Utc
245            }
246            GeneralizedTimeAllowedTimezone::Any => {
247                if remaining == b"Z" {
248                    Zone::Utc
249                } else {
250                    if remaining.len() != 5 {
251                        return Err(source.content_err(format!(
252                            "`{}` is not a 5 character timezone identifier",
253                            String::from_utf8_lossy(remaining)
254                        )));
255                    }
256
257                    let east = match remaining[0] {
258                        b'+' => true,
259                        b'-' => false,
260                        _ => {
261                            return Err(source.content_err(format!(
262                                "`{}` does not begin with a +- offset",
263                                String::from_utf8_lossy(remaining)
264                            )))
265                        }
266                    };
267
268                    let offset_hours = u32::from_str(
269                        std::str::from_utf8(&remaining[1..3])
270                            .map_err(|s| source.content_err(s.to_string()))?,
271                    )
272                    .map_err(|s| source.content_err(s.to_string()))?;
273                    let offset_minutes = u32::from_str(
274                        std::str::from_utf8(&remaining[3..])
275                            .map_err(|s| source.content_err(s.to_string()))?,
276                    )
277                    .map_err(|s| source.content_err(s.to_string()))?;
278
279                    let offset_seconds = (offset_hours * 3600 + offset_minutes * 60) as i32;
280
281                    Zone::Offset(if east {
282                        chrono::FixedOffset::east_opt(offset_seconds)
283                            .ok_or_else(|| source.content_err("bad timezone time"))?
284                    } else {
285                        chrono::FixedOffset::west_opt(offset_seconds)
286                            .ok_or_else(|| source.content_err("bad timezone offset"))?
287                    })
288                }
289            }
290        };
291
292        if let chrono::LocalResult::Single(dt) =
293            chrono::Utc.with_ymd_and_hms(year, month, day, hour, minute, second)
294        {
295            if let Some(dt) = dt.with_nanosecond(nano) {
296                Ok(Self {
297                    time: dt.naive_utc(),
298                    fractional_seconds: allow_fractional_seconds,
299                    timezone,
300                })
301            } else {
302                Err(source.content_err("invalid time value"))
303            }
304        } else {
305            Err(source.content_err("invalid datetime value"))
306        }
307    }
308}
309
310impl Display for GeneralizedTime {
311    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
312        let str = format!(
313            "{}{}",
314            self.time.format(if self.fractional_seconds {
315                "%Y%m%d%H%M%S%.f"
316            } else {
317                "%Y%m%d%H%M%S"
318            }),
319            self.timezone
320        );
321        write!(f, "{}", str)
322    }
323}
324
325impl From<GeneralizedTime> for chrono::DateTime<chrono::Utc> {
326    fn from(gt: GeneralizedTime) -> Self {
327        match gt.timezone {
328            Zone::Utc => {
329                chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(gt.time, chrono::Utc)
330            }
331            Zone::Offset(offset) => chrono::DateTime::<chrono::Utc>::from_naive_utc_and_offset(
332                gt.time.add(offset),
333                chrono::Utc,
334            ),
335        }
336    }
337}
338
339impl From<chrono::DateTime<chrono::Utc>> for GeneralizedTime {
340    fn from(utc: chrono::DateTime<chrono::Utc>) -> Self {
341        Self {
342            time: utc.naive_utc(),
343            fractional_seconds: utc.timestamp_subsec_micros() > 0,
344            timezone: Zone::Utc,
345        }
346    }
347}
348
349impl PrimitiveContent for GeneralizedTime {
350    const TAG: Tag = Tag::GENERALIZED_TIME;
351
352    fn encoded_len(&self, _: Mode) -> usize {
353        self.to_string().len()
354    }
355
356    fn write_encoded<W: Write>(&self, _: Mode, target: &mut W) -> Result<(), std::io::Error> {
357        target.write_all(self.to_string().as_bytes())
358    }
359}
360
361#[derive(Clone, Debug, Eq, PartialEq)]
362pub struct UtcTime(chrono::DateTime<chrono::Utc>);
363
364impl From<chrono::DateTime<chrono::Utc>> for UtcTime {
365    fn from(value: chrono::DateTime<chrono::Utc>) -> Self {
366        Self(value)
367    }
368}
369
370impl From<UtcTime> for chrono::DateTime<chrono::Utc> {
371    fn from(value: UtcTime) -> Self {
372        *value.deref()
373    }
374}
375
376impl UtcTime {
377    /// Obtain a new instance with now as the time.
378    pub fn now() -> Self {
379        Self(chrono::Utc::now())
380    }
381
382    pub fn take_from<S: Source>(cons: &mut Constructed<S>) -> Result<Self, DecodeError<S::Error>> {
383        cons.take_primitive_if(Tag::UTC_TIME, |prim| Self::from_primitive(prim))
384    }
385
386    pub fn from_primitive<S: Source>(
387        prim: &mut Primitive<S>,
388    ) -> Result<Self, DecodeError<S::Error>> {
389        let data = prim.take_all()?;
390
391        if data.len() != "YYMMDDHHMMSSZ".len() {
392            return Err(prim.content_err("UTCTime not of expected length"));
393        }
394
395        let year = i32::from_str(
396            std::str::from_utf8(&data[0..2]).map_err(|s| prim.content_err(s.to_string()))?,
397        )
398        .map_err(|s| prim.content_err(s.to_string()))?;
399
400        let year = if year >= 50 { year + 1900 } else { year + 2000 };
401
402        let month = u32::from_str(
403            std::str::from_utf8(&data[2..4]).map_err(|s| prim.content_err(s.to_string()))?,
404        )
405        .map_err(|s| prim.content_err(s.to_string()))?;
406        let day = u32::from_str(
407            std::str::from_utf8(&data[4..6]).map_err(|s| prim.content_err(s.to_string()))?,
408        )
409        .map_err(|s| prim.content_err(s.to_string()))?;
410        let hour = u32::from_str(
411            std::str::from_utf8(&data[6..8]).map_err(|s| prim.content_err(s.to_string()))?,
412        )
413        .map_err(|s| prim.content_err(s.to_string()))?;
414        let minute = u32::from_str(
415            std::str::from_utf8(&data[8..10]).map_err(|s| prim.content_err(s.to_string()))?,
416        )
417        .map_err(|s| prim.content_err(s.to_string()))?;
418        let second = u32::from_str(
419            std::str::from_utf8(&data[10..12]).map_err(|s| prim.content_err(s.to_string()))?,
420        )
421        .map_err(|s| prim.content_err(s.to_string()))?;
422
423        if data[12] != b'Z' {
424            return Err(prim.content_err("UTCTime must end with `Z`"));
425        }
426
427        if let chrono::LocalResult::Single(dt) =
428            chrono::Utc.with_ymd_and_hms(year, month, day, hour, minute, second)
429        {
430            Ok(Self(dt))
431        } else {
432            Err(prim.content_err("invalid year month day hour minute second value"))
433        }
434    }
435}
436
437impl Display for UtcTime {
438    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
439        let str = format!(
440            "{:02}{:02}{:02}{:02}{:02}{:02}Z",
441            self.0.year() % 100,
442            self.0.month(),
443            self.0.day(),
444            self.0.hour(),
445            self.0.minute(),
446            self.0.second()
447        );
448        write!(f, "{}", str)
449    }
450}
451
452impl Deref for UtcTime {
453    type Target = chrono::DateTime<chrono::Utc>;
454
455    fn deref(&self) -> &Self::Target {
456        &self.0
457    }
458}
459
460impl PrimitiveContent for UtcTime {
461    const TAG: Tag = Tag::UTC_TIME;
462
463    fn encoded_len(&self, _: Mode) -> usize {
464        self.to_string().len()
465    }
466
467    fn write_encoded<W: Write>(&self, _: Mode, target: &mut W) -> Result<(), std::io::Error> {
468        target.write_all(self.to_string().as_bytes())
469    }
470}
471
472#[cfg(test)]
473mod test {
474    use {super::*, bcder::decode::ContentError};
475
476    #[test]
477    fn generalized_time() -> Result<(), ContentError> {
478        let gt = GeneralizedTime {
479            time: chrono::DateTime::from_timestamp(1643510772, 0)
480                .unwrap()
481                .naive_utc(),
482            fractional_seconds: false,
483            timezone: Zone::Utc,
484        };
485        assert_eq!(gt.to_string(), "20220130024612Z");
486
487        let gt = GeneralizedTime {
488            time: chrono::DateTime::from_timestamp(1643510772, 0)
489                .unwrap()
490                .naive_utc(),
491            fractional_seconds: false,
492            timezone: Zone::Offset(chrono::FixedOffset::east_opt(3600).unwrap()),
493        };
494        assert_eq!(gt.to_string(), "20220130024612+0100");
495
496        let gt = GeneralizedTime {
497            time: chrono::DateTime::from_timestamp(1643510772, 0)
498                .unwrap()
499                .naive_utc(),
500            fractional_seconds: false,
501            timezone: Zone::Offset(chrono::FixedOffset::west_opt(7200).unwrap()),
502        };
503        assert_eq!(gt.to_string(), "20220130024612-0200");
504
505        let gt = GeneralizedTime::parse(
506            SliceSource::new(b"20220129133742Z"),
507            true,
508            GeneralizedTimeAllowedTimezone::Z,
509        )?;
510        assert_eq!(gt.time.year(), 2022);
511        assert_eq!(gt.time.month(), 1);
512        assert_eq!(gt.time.day(), 29);
513        assert_eq!(gt.time.hour(), 13);
514        assert_eq!(gt.time.minute(), 37);
515        assert_eq!(gt.time.second(), 42);
516        assert_eq!(gt.time.nanosecond(), 0);
517        assert_eq!(format!("{}", gt.timezone), "Z");
518
519        assert_eq!(gt.to_string(), "20220129133742Z");
520
521        let gt = GeneralizedTime::parse(
522            SliceSource::new(b"20220129133742.333Z"),
523            true,
524            GeneralizedTimeAllowedTimezone::Z,
525        )?;
526        assert_eq!(gt.to_string(), "20220129133742.333Z");
527
528        let gt = GeneralizedTime::parse(
529            SliceSource::new(b"20220129133742-0800"),
530            false,
531            GeneralizedTimeAllowedTimezone::Any,
532        )?;
533        assert_eq!(format!("{}", gt.timezone), "-0800");
534
535        let gt = GeneralizedTime::parse(
536            SliceSource::new(b"20220129133742+1000"),
537            false,
538            GeneralizedTimeAllowedTimezone::Any,
539        )?;
540        assert_eq!(format!("{}", gt.timezone), "+1000");
541
542        let gt = GeneralizedTime::parse(
543            SliceSource::new(b"20220129133742.333-0800"),
544            true,
545            GeneralizedTimeAllowedTimezone::Any,
546        )?;
547        assert_eq!(gt.to_string(), "20220129133742.333-0800");
548
549        Ok(())
550    }
551
552    #[test]
553    fn generalized_time_invalid() {
554        for allow_fractional_seconds in [false, true] {
555            for allowed_timezone in [
556                GeneralizedTimeAllowedTimezone::Any,
557                GeneralizedTimeAllowedTimezone::Z,
558            ] {
559                assert!(GeneralizedTime::parse(
560                    SliceSource::new(b""),
561                    allow_fractional_seconds,
562                    allowed_timezone
563                )
564                .is_err());
565                assert!(GeneralizedTime::parse(
566                    SliceSource::new(b"abcd"),
567                    allow_fractional_seconds,
568                    allowed_timezone
569                )
570                .is_err());
571                assert!(GeneralizedTime::parse(
572                    SliceSource::new(b"2022"),
573                    allow_fractional_seconds,
574                    allowed_timezone
575                )
576                .is_err());
577                assert!(GeneralizedTime::parse(
578                    SliceSource::new(b"202201"),
579                    allow_fractional_seconds,
580                    allowed_timezone
581                )
582                .is_err());
583                assert!(GeneralizedTime::parse(
584                    SliceSource::new(b"20220130"),
585                    allow_fractional_seconds,
586                    allowed_timezone
587                )
588                .is_err());
589                assert!(GeneralizedTime::parse(
590                    SliceSource::new(b"2022013012"),
591                    allow_fractional_seconds,
592                    allowed_timezone
593                )
594                .is_err());
595                assert!(GeneralizedTime::parse(
596                    SliceSource::new(b"202201301230"),
597                    allow_fractional_seconds,
598                    allowed_timezone
599                )
600                .is_err());
601                assert!(GeneralizedTime::parse(
602                    SliceSource::new(b"20220130123015"),
603                    allow_fractional_seconds,
604                    allowed_timezone
605                )
606                .is_err());
607                assert!(GeneralizedTime::parse(
608                    SliceSource::new(b"20220130123015."),
609                    allow_fractional_seconds,
610                    allowed_timezone
611                )
612                .is_err());
613                assert!(GeneralizedTime::parse(
614                    SliceSource::new(b"20220130123015.a"),
615                    allow_fractional_seconds,
616                    allowed_timezone
617                )
618                .is_err());
619                assert!(GeneralizedTime::parse(
620                    SliceSource::new(b"20220130123015.0a"),
621                    allow_fractional_seconds,
622                    allowed_timezone
623                )
624                .is_err());
625                assert!(GeneralizedTime::parse(
626                    SliceSource::new(b"20220130123015a"),
627                    allow_fractional_seconds,
628                    allowed_timezone
629                )
630                .is_err());
631                assert!(GeneralizedTime::parse(
632                    SliceSource::new(b"20220130123015-"),
633                    allow_fractional_seconds,
634                    allowed_timezone
635                )
636                .is_err());
637                assert!(GeneralizedTime::parse(
638                    SliceSource::new(b"20220130123015+"),
639                    allow_fractional_seconds,
640                    allowed_timezone
641                )
642                .is_err());
643                assert!(GeneralizedTime::parse(
644                    SliceSource::new(b"20220130123015+01"),
645                    allow_fractional_seconds,
646                    allowed_timezone
647                )
648                .is_err());
649                assert!(GeneralizedTime::parse(
650                    SliceSource::new(b"20220130123015+01000"),
651                    allow_fractional_seconds,
652                    allowed_timezone
653                )
654                .is_err());
655                assert!(GeneralizedTime::parse(
656                    SliceSource::new(b"20220130123015+0100a"),
657                    allow_fractional_seconds,
658                    allowed_timezone
659                )
660                .is_err());
661                assert!(GeneralizedTime::parse(
662                    SliceSource::new(b"20220130123015-01000"),
663                    allow_fractional_seconds,
664                    allowed_timezone
665                )
666                .is_err());
667                assert!(GeneralizedTime::parse(
668                    SliceSource::new(b"20220130123015-0100a"),
669                    allow_fractional_seconds,
670                    allowed_timezone
671                )
672                .is_err());
673            }
674        }
675    }
676}