Skip to main content

rc_crypto/certificate/
validity.rs

1// Copyright 2026-Present Datadog, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use jiff::Timestamp;
16use std::{fmt::Display, ops::RangeInclusive};
17use x509_parser::{prelude::X509Certificate, time::ASN1Time};
18
19use crate::cached_string_repr::CachedStringRepr;
20
21/// The [`Validity`] is the time interval during which a [`Certificate`] is
22/// considered valid.
23///
24/// An X.509 certificate validity period is defined in [RFC 5280 § 4.1.2.5] as
25/// a `SEQUENCE` of two date-time values: `notBefore` and `notAfter`. The
26/// certificate is considered valid only during the closed interval
27/// `[notBefore, notAfter]`.
28///
29/// [`Certificate`]: super::Certificate
30/// [RFC 5280 § 4.1.2.5]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5
31#[derive(Debug, Clone)]
32pub struct Validity {
33    /// The earliest date and time at which the certificate is considered valid.
34    not_before: Timestamp,
35
36    /// The latest date and time at which the certificate is considered valid.
37    not_after: Timestamp,
38
39    /// A lazily-rendered string representation of `not_before` & `not_after`.
40    ///
41    /// See [`Self::as_display_str()`] for initialisation.
42    rendered: CachedStringRepr,
43}
44
45impl Validity {
46    /// Construct a new [`Validity`] from the x509_parser Validity representation.
47    ///
48    /// x509_parser represents timestamps as [`ASN1Time`], which exposes the
49    /// underlying Unix timestamp in seconds. Each value is converted to a
50    /// [`jiff::Timestamp`] for ergonomic use.
51    ///
52    /// # Errors
53    ///
54    /// Returns an error if either timestamp in `value` is outside of the valid
55    /// [`jiff::Timestamp`] range.
56    fn new(value: &x509_parser::certificate::Validity) -> Result<Self, jiff::Error> {
57        let not_before = asn1_to_timestamp(value.not_before)?;
58        let not_after = asn1_to_timestamp(value.not_after)?;
59
60        Ok(Self {
61            not_before,
62            not_after,
63            rendered: Default::default(),
64        })
65    }
66
67    /// Return the [`Timestamp`] at which the certificate becomes valid.
68    pub fn not_before_as_timestamp(&self) -> Timestamp {
69        self.not_before
70    }
71
72    /// Return the [`Timestamp`] at which the certificate expires.
73    pub fn not_after_as_timestamp(&self) -> Timestamp {
74        self.not_after
75    }
76
77    /// Render `notBefore` and `notAfter` of [`Timestamp`] as a single string
78    ///
79    /// Example `2024-01-01T00:00:00Z..2025-01-01T00:00:00Z`
80    ///
81    /// This value is lazily rendered and cached for reuse.
82    pub fn as_display_str(&self) -> &str {
83        self.rendered
84            .get_or_init(|| format!("{}..{}", self.not_before, self.not_after))
85    }
86
87    /// Return the validity as a RangeInclusive.
88    ///
89    /// From [RFC 5280 § 4.1.2.5]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.5
90    /// the validity period for a certificate is the period of time from
91    /// notBefore through notAfter, inclusive
92    pub fn range(&self) -> RangeInclusive<Timestamp> {
93        RangeInclusive::new(self.not_before, self.not_after)
94    }
95}
96
97impl Display for Validity {
98    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99        self.as_display_str().fmt(f)
100    }
101}
102
103/// Convert an [`ASN1Time`] to a [`Timestamp`] by extracting the underlying
104/// Unix timestamp in seconds.
105///
106/// # Errors
107///
108/// Returns an error if the seconds value is outside the range supported by
109/// [`Timestamp`] (approximately ±9999 years from the Unix epoch).
110fn asn1_to_timestamp(timestamp: ASN1Time) -> Result<Timestamp, jiff::Error> {
111    let secs = timestamp.timestamp();
112    Timestamp::from_second(secs)
113}
114
115impl<'a> TryFrom<&'a X509Certificate<'a>> for Validity {
116    type Error = jiff::Error;
117
118    fn try_from(cert: &'a X509Certificate<'a>) -> Result<Self, Self::Error> {
119        Self::new(cert.validity())
120    }
121}
122
123/// Render a [`Validity`] as an encoded string in structured logging.
124impl valuable::Valuable for Validity {
125    fn as_value(&self) -> valuable::Value<'_> {
126        valuable::Value::String(self.as_display_str())
127    }
128
129    fn visit(&self, visit: &mut dyn valuable::Visit) {
130        visit.visit_value(self.as_value());
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use jiff::Timestamp;
137    use proptest::prelude::*;
138
139    use super::*;
140
141    fn make_validity(not_before_secs: i64, not_after_secs: i64) -> Validity {
142        Validity {
143            not_before: Timestamp::from_second(not_before_secs).unwrap(),
144            not_after: Timestamp::from_second(not_after_secs).unwrap(),
145            rendered: Default::default(),
146        }
147    }
148
149    #[test]
150    fn test_fixture() {
151        let not_before_secs = 1_000_000_000i64; // 2001-09-09T01:46:40Z
152        let not_after_secs = 2_000_000_000i64; // 2033-05-18T03:33:20Z
153
154        let v = make_validity(not_before_secs, not_after_secs);
155
156        assert_eq!(
157            v.not_before_as_timestamp(),
158            Timestamp::from_second(not_before_secs).unwrap()
159        );
160
161        assert_eq!(
162            v.not_after_as_timestamp(),
163            Timestamp::from_second(not_after_secs).unwrap()
164        );
165
166        assert_eq!(
167            v.as_display_str(),
168            "2001-09-09T01:46:40Z..2033-05-18T03:33:20Z"
169        );
170    }
171
172    // Verify that trying to convert a timestamp outside of [`jiff::Timestamp`]'s
173    // MIN & MAX bounds returns an error
174    #[test]
175    fn test_timestamp_out_of_range() {
176        // Set timestamp to +1 of the jiff::Timestamp::MAX
177        let jiff_beyond_max = Timestamp::MAX.as_second() + 1;
178        assert!(Timestamp::from_second(jiff_beyond_max).is_err());
179        let asn1_beyond_max = ASN1Time::from_timestamp(jiff_beyond_max).unwrap();
180        assert!(asn1_to_timestamp(asn1_beyond_max).is_err());
181
182        // Set timestamp to -1 of the jiff::Timestamp::MIN
183        let jiff_before_min = Timestamp::MIN.as_second() - 1;
184        assert!(Timestamp::from_second(jiff_before_min).is_err());
185        let asn1_before_min = ASN1Time::from_timestamp(jiff_before_min).unwrap();
186        assert!(asn1_to_timestamp(asn1_before_min).is_err());
187    }
188
189    proptest! {
190        /// This tests both timestamps that are in AND out of the Validity range.
191        /// For a given timestamp and validity range (not_before, not_after), checks
192        /// whether timestamp is 'contained' within the range.
193        #[test]
194        fn prop_timestamp_in_validity_range(
195            not_before in 0i64..=3_000_000_000i64,
196            not_after in 0i64..=3_000_000_000i64,
197            ts_secs in 0i64..=4_000_000_000i64,
198        ) {
199            let v = make_validity(not_before, not_after);
200            let ts = Timestamp::from_second(ts_secs).unwrap();
201
202            // Verify that the RangeInclusive of not_before and not_after
203            // correctly catorizes whether the timestamp is within the validity range.
204            prop_assert_eq!(v.range().contains(&ts), not_before <= ts_secs && ts_secs <= not_after);
205        }
206    }
207}