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}