|
| 1 | +use core::str::FromStr; |
| 2 | +use core::time::Duration; |
| 3 | +use der::{ |
| 4 | + DateTime, DecodeValue, EncodeValue, ErrorKind, FixedTag, Header, Length, Reader, Result, Tag, |
| 5 | + Writer, |
| 6 | +}; |
| 7 | + |
| 8 | +/// ASN.1 `GeneralizedTime` type. |
| 9 | +/// |
| 10 | +/// This type implements the validity requirements specified in |
| 11 | +/// X.690 DER encoding of GeneralizedTime: |
| 12 | +/// |
| 13 | +/// > 11.7.1 - The encoding shall terminate with a "Z" |
| 14 | +/// > 11.7.2 - The seconds element shall always be present. |
| 15 | +/// > 11.7.3 - The fractional-seconds elements, if present, shall omit all |
| 16 | +/// > trailing zeros; if the elements correspond to 0, they shall be wholly |
| 17 | +/// > omitted, and the decimal point element also shall be omitted |
| 18 | +/// > 11.7.4 - The decimal point element, if present, shall be the point option ".". |
| 19 | +/// > 11.7.5 - Midnight (GMT) shall be represented in the form `YYYYMMDD000000Z` |
| 20 | +/// > where `YYYYMMDD` represents the day following the midnight in question |
| 21 | +#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] |
| 22 | +pub struct GeneralizedTimeNanos { |
| 23 | + datetime: DateTime, |
| 24 | + |
| 25 | + /// Nanoseconds (0-999 999 999) |
| 26 | + nanoseconds: u32, |
| 27 | +} |
| 28 | + |
| 29 | +impl GeneralizedTimeNanos { |
| 30 | + /// Length of an RFC 5280-flavored ASN.1 DER-encoded [`GeneralizedTimeNanos`]. |
| 31 | + const MIN_LENGTH: usize = 15; |
| 32 | + |
| 33 | + /// Maximum length of a GeneralizedTime containing nanoseconds. |
| 34 | + const MAX_LENGTH: usize = Self::MIN_LENGTH + 10; |
| 35 | + |
| 36 | + /// Get the duration of this timestamp since `UNIX_EPOCH`. |
| 37 | + pub fn to_unix_duration(&self) -> Duration { |
| 38 | + self.datetime.unix_duration() + Duration::from_nanos(u64::from(self.nanoseconds)) |
| 39 | + } |
| 40 | +} |
| 41 | + |
| 42 | +/// Decode 2-digit decimal value |
| 43 | +#[allow(clippy::arithmetic_side_effects)] |
| 44 | +fn decode_decimal(tag: Tag, hi: u8, lo: u8) -> Result<u8> { |
| 45 | + if hi.is_ascii_digit() && lo.is_ascii_digit() { |
| 46 | + Ok((hi - b'0') * 10 + (lo - b'0')) |
| 47 | + } else { |
| 48 | + Err(tag.value_error().into()) |
| 49 | + } |
| 50 | +} |
| 51 | + |
| 52 | +/// Encode 2-digit decimal value |
| 53 | +fn encode_decimal<W>(writer: &mut W, tag: Tag, value: u8) -> Result<()> |
| 54 | +where |
| 55 | + W: Writer + ?Sized, |
| 56 | +{ |
| 57 | + let hi_val = value / 10; |
| 58 | + |
| 59 | + if hi_val >= 10 { |
| 60 | + return Err(tag.value_error().into()); |
| 61 | + } |
| 62 | + |
| 63 | + writer.write_byte(b'0'.checked_add(hi_val).ok_or(ErrorKind::Overflow)?)?; |
| 64 | + writer.write_byte(b'0'.checked_add(value % 10).ok_or(ErrorKind::Overflow)?) |
| 65 | +} |
| 66 | + |
| 67 | +/// Decode up to 9 digits of fractional seconds, returns them as nanoseconds. |
| 68 | +/// |
| 69 | +/// Assumes DER encoding rules, so no trailing zeroes. |
| 70 | +fn decode_fractional_secs(tag: Tag, fract: &[u8]) -> Result<u32> { |
| 71 | + // An empty fractional should be checked for by the caller, and we only |
| 72 | + // support up to 9 digits (for nanoseconds accuracy). |
| 73 | + if fract.is_empty() || fract.len() > 9 { |
| 74 | + return Err(tag.value_error().into()); |
| 75 | + } |
| 76 | + |
| 77 | + // The fractional seconds should not containing trailing zeros according |
| 78 | + // to DER encoding rules. |
| 79 | + if fract.last() == Some(&b'0') { |
| 80 | + return Err(tag.value_error().into()); |
| 81 | + } |
| 82 | + |
| 83 | + // We're going to use u32::from_str, which accepts a leading + sign. |
| 84 | + // So make sure the leading digit is not a + but a proper digit. |
| 85 | + if !fract[0].is_ascii_digit() { |
| 86 | + return Err(tag.value_error().into()); |
| 87 | + } |
| 88 | + |
| 89 | + // Decode the number. Use u32::from_ascii when it stabilizes, so we don't |
| 90 | + // pay the unnecessary cost of utf8 checking. |
| 91 | + let fract_str = core::str::from_utf8(fract).map_err(|_| tag.value_error())?; |
| 92 | + let fract_num = u32::from_str(fract_str).map_err(|_| tag.value_error())?; |
| 93 | + |
| 94 | + // Multiply the number to turn it into a nanosecond figure. |
| 95 | + let out = fract_num * 10_u32.pow(9 - u32::try_from(fract.len())?); |
| 96 | + |
| 97 | + Ok(out) |
| 98 | +} |
| 99 | + |
| 100 | +fn for_each_digits_without_trailing_zeroes<F>(mut nanoseconds: u32, mut f: F) -> Result<()> |
| 101 | +where |
| 102 | + F: FnMut(u8) -> Result<()>, |
| 103 | +{ |
| 104 | + let mut idx = 100_000_000; |
| 105 | + while nanoseconds != 0 && idx != 0 { |
| 106 | + let cur_val = u8::try_from((nanoseconds / idx) % 10).map_err(|_| ErrorKind::Overflow)?; |
| 107 | + nanoseconds -= u32::from(cur_val) * idx; |
| 108 | + idx /= 10; |
| 109 | + f(cur_val)? |
| 110 | + } |
| 111 | + Ok(()) |
| 112 | +} |
| 113 | + |
| 114 | +/// Encodes the given nanoseconds as fractional secs, discarding trailing |
| 115 | +/// zeroes. |
| 116 | +fn encode_fractional_secs<W>(writer: &mut W, tag: Tag, nanoseconds: u32) -> Result<()> |
| 117 | +where |
| 118 | + W: Writer + ?Sized, |
| 119 | +{ |
| 120 | + // We should never have more than 999_999_999 nanoseconds in a second. |
| 121 | + if nanoseconds >= 1_000_000_000 { |
| 122 | + return Err(tag.value_error().into()); |
| 123 | + } |
| 124 | + |
| 125 | + // if u32::format_into ever gets stabilized, this can be better written as |
| 126 | + // |
| 127 | + // ``` |
| 128 | + // let mut buf = NumBuffer::new(); |
| 129 | + // let s = value.format_into(&mut buf); |
| 130 | + // s.trim_end_matches('0'); |
| 131 | + // writer.write(s.as_bytes())?; |
| 132 | + // ``` |
| 133 | + // |
| 134 | + // This would benefit from using the standard library implementation of |
| 135 | + // formatting number, which is much more optimized, using LUTs to avoid a |
| 136 | + // lot of the work and handling multiple digits simultaneously. |
| 137 | + for_each_digits_without_trailing_zeroes(nanoseconds, |cur_val| { |
| 138 | + writer.write_byte(b'0'.checked_add(cur_val).ok_or(ErrorKind::Overflow)?) |
| 139 | + })?; |
| 140 | + Ok(()) |
| 141 | +} |
| 142 | + |
| 143 | +/// Creates a [`GeneralizedTimeNanos`] from its individual, ascii |
| 144 | +/// encoded components. |
| 145 | +#[allow(clippy::too_many_arguments, reason = "Simple helper function")] |
| 146 | +fn decode_from_values( |
| 147 | + year: (u8, u8, u8, u8), |
| 148 | + month: (u8, u8), |
| 149 | + day: (u8, u8), |
| 150 | + hour: (u8, u8), |
| 151 | + min: (u8, u8), |
| 152 | + sec: (u8, u8), |
| 153 | + fract: Option<&[u8]>, |
| 154 | +) -> Result<GeneralizedTimeNanos> { |
| 155 | + let year = u16::from(decode_decimal(GeneralizedTimeNanos::TAG, year.0, year.1)?) |
| 156 | + .checked_mul(100) |
| 157 | + .and_then(|y| { |
| 158 | + y.checked_add( |
| 159 | + decode_decimal(GeneralizedTimeNanos::TAG, year.2, year.3) |
| 160 | + .ok()? |
| 161 | + .into(), |
| 162 | + ) |
| 163 | + }) |
| 164 | + .ok_or(ErrorKind::DateTime)?; |
| 165 | + let month = decode_decimal(GeneralizedTimeNanos::TAG, month.0, month.1)?; |
| 166 | + let day = decode_decimal(GeneralizedTimeNanos::TAG, day.0, day.1)?; |
| 167 | + let hour = decode_decimal(GeneralizedTimeNanos::TAG, hour.0, hour.1)?; |
| 168 | + let minute = decode_decimal(GeneralizedTimeNanos::TAG, min.0, min.1)?; |
| 169 | + let second = decode_decimal(GeneralizedTimeNanos::TAG, sec.0, sec.1)?; |
| 170 | + |
| 171 | + let nanoseconds = if let Some(fract) = fract { |
| 172 | + decode_fractional_secs(GeneralizedTimeNanos::TAG, fract)? |
| 173 | + } else { |
| 174 | + 0 |
| 175 | + }; |
| 176 | + |
| 177 | + let datetime = DateTime::new(year, month, day, hour, minute, second) |
| 178 | + .map_err(|_| GeneralizedTimeNanos::TAG.value_error())?; |
| 179 | + |
| 180 | + Ok(GeneralizedTimeNanos { |
| 181 | + datetime, |
| 182 | + nanoseconds, |
| 183 | + }) |
| 184 | +} |
| 185 | + |
| 186 | +impl<'a> DecodeValue<'a> for GeneralizedTimeNanos { |
| 187 | + type Error = der::Error; |
| 188 | + |
| 189 | + #[rustfmt::skip] // Keep the match readable on a single line. |
| 190 | + fn decode_value<R: Reader<'a>>(reader: &mut R, header: Header) -> Result<Self> { |
| 191 | + let len = usize::try_from(header.length())?; |
| 192 | + if !(Self::MIN_LENGTH..=Self::MAX_LENGTH).contains(&len) { |
| 193 | + return Err(Self::TAG.value_error().into()); |
| 194 | + } |
| 195 | + |
| 196 | + let mut bytes = [0u8; Self::MAX_LENGTH]; |
| 197 | + let data = reader.read_into(&mut bytes[..len])?; |
| 198 | + |
| 199 | + match data { |
| 200 | + // No nanoseconds |
| 201 | + [y1, y2, y3, y4, mon1, mon2, day1, day2, hour1, hour2, min1, min2, sec1, sec2, b'Z'] => |
| 202 | + decode_from_values((*y1, *y2, *y3, *y4), (*mon1, *mon2), (*day1, *day2), |
| 203 | + (*hour1, *hour2), (*min1, *min2), (*sec1, *sec2), None), |
| 204 | + // With nanoseconds |
| 205 | + [y1, y2, y3, y4, mon1, mon2, day1, day2, hour1, hour2, min1, min2, sec1, sec2, b'.', fract @ .., b'Z'] => |
| 206 | + decode_from_values((*y1, *y2, *y3, *y4), (*mon1, *mon2), (*day1, *day2), |
| 207 | + (*hour1, *hour2), (*min1, *min2), (*sec1, *sec2), Some(fract)), |
| 208 | + _ => Err(Self::TAG.value_error().into()), |
| 209 | + } |
| 210 | + } |
| 211 | +} |
| 212 | + |
| 213 | +impl EncodeValue for GeneralizedTimeNanos { |
| 214 | + fn value_len(&self) -> Result<Length> { |
| 215 | + let mut len = Self::MIN_LENGTH; |
| 216 | + if self.nanoseconds != 0 { |
| 217 | + // Count the number of digits we'll encode. |
| 218 | + for_each_digits_without_trailing_zeroes(self.nanoseconds, |_| { |
| 219 | + len += 1; |
| 220 | + Ok(()) |
| 221 | + })?; |
| 222 | + // + 1 for the decimal separator. |
| 223 | + len += 1; |
| 224 | + } |
| 225 | + Length::try_from(len) |
| 226 | + } |
| 227 | + |
| 228 | + fn encode_value(&self, writer: &mut impl Writer) -> Result<()> { |
| 229 | + let year_hi = u8::try_from(self.datetime.year() / 100)?; |
| 230 | + let year_lo = u8::try_from(self.datetime.year() % 100)?; |
| 231 | + |
| 232 | + encode_decimal(writer, Self::TAG, year_hi)?; |
| 233 | + encode_decimal(writer, Self::TAG, year_lo)?; |
| 234 | + encode_decimal(writer, Self::TAG, self.datetime.month())?; |
| 235 | + encode_decimal(writer, Self::TAG, self.datetime.day())?; |
| 236 | + encode_decimal(writer, Self::TAG, self.datetime.hour())?; |
| 237 | + encode_decimal(writer, Self::TAG, self.datetime.minutes())?; |
| 238 | + encode_decimal(writer, Self::TAG, self.datetime.seconds())?; |
| 239 | + if self.nanoseconds != 0 { |
| 240 | + writer.write_byte(b'.')?; |
| 241 | + encode_fractional_secs(writer, Self::TAG, self.nanoseconds)?; |
| 242 | + } |
| 243 | + writer.write_byte(b'Z') |
| 244 | + } |
| 245 | +} |
| 246 | + |
| 247 | +impl FixedTag for GeneralizedTimeNanos { |
| 248 | + const TAG: Tag = Tag::GeneralizedTime; |
| 249 | +} |
| 250 | + |
| 251 | +#[cfg(test)] |
| 252 | +#[allow(clippy::unwrap_used)] |
| 253 | +mod tests { |
| 254 | + use super::GeneralizedTimeNanos; |
| 255 | + use der::{Decode, Encode, SliceWriter}; |
| 256 | + use hex_literal::hex; |
| 257 | + |
| 258 | + fn round_trip(der: &[u8], expected_timestamp: u64, expected_nanos: u32) { |
| 259 | + let utc_time = GeneralizedTimeNanos::from_der(der).unwrap(); |
| 260 | + assert_eq!(utc_time.to_unix_duration().as_secs(), expected_timestamp); |
| 261 | + assert_eq!(utc_time.to_unix_duration().subsec_nanos(), expected_nanos); |
| 262 | + |
| 263 | + let mut buf = [0u8; 128]; |
| 264 | + let mut encoder = SliceWriter::new(&mut buf); |
| 265 | + utc_time.encode(&mut encoder).unwrap(); |
| 266 | + assert_eq!(der, encoder.finish().unwrap()); |
| 267 | + } |
| 268 | + |
| 269 | + #[test] |
| 270 | + fn round_trip_normal() { |
| 271 | + let example_bytes = hex!("18 0f 31 39 39 31 30 35 30 36 32 33 34 35 34 30 5a"); |
| 272 | + round_trip(&example_bytes, 673573540, 0); |
| 273 | + } |
| 274 | + |
| 275 | + #[test] |
| 276 | + fn round_trip_nanoseconds_100_000_000() { |
| 277 | + let example_bytes = hex!("18 11 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 5A"); |
| 278 | + round_trip(&example_bytes, 1728312610, 100_000_000); |
| 279 | + } |
| 280 | + |
| 281 | + #[test] |
| 282 | + fn round_trip_nanoseconds_120_000_000() { |
| 283 | + let example_bytes = hex!("18 12 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 5A"); |
| 284 | + round_trip(&example_bytes, 1728312610, 120_000_000); |
| 285 | + } |
| 286 | + |
| 287 | + #[test] |
| 288 | + fn round_trip_nanoseconds_123_000_000() { |
| 289 | + let example_bytes = hex!("18 13 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 5A"); |
| 290 | + round_trip(&example_bytes, 1728312610, 123_000_000); |
| 291 | + } |
| 292 | + |
| 293 | + #[test] |
| 294 | + fn round_trip_nanoseconds_123_400_000() { |
| 295 | + let example_bytes = |
| 296 | + hex!("18 14 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 5A"); |
| 297 | + round_trip(&example_bytes, 1728312610, 123_400_000); |
| 298 | + } |
| 299 | + |
| 300 | + #[test] |
| 301 | + fn round_trip_nanoseconds_123_450_000() { |
| 302 | + let example_bytes = |
| 303 | + hex!("18 15 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 35 5A"); |
| 304 | + round_trip(&example_bytes, 1728312610, 123_450_000); |
| 305 | + } |
| 306 | + |
| 307 | + #[test] |
| 308 | + fn round_trip_nanoseconds_123_456_000() { |
| 309 | + let example_bytes = |
| 310 | + hex!("18 16 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 35 36 5A"); |
| 311 | + round_trip(&example_bytes, 1728312610, 123_456_000); |
| 312 | + } |
| 313 | + |
| 314 | + #[test] |
| 315 | + fn round_trip_nanoseconds_123_456_700() { |
| 316 | + let example_bytes = |
| 317 | + hex!("18 17 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 35 36 37 5A"); |
| 318 | + round_trip(&example_bytes, 1728312610, 123_456_700); |
| 319 | + } |
| 320 | + |
| 321 | + #[test] |
| 322 | + fn round_trip_nanoseconds_123_456_780() { |
| 323 | + let example_bytes = |
| 324 | + hex!("18 18 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 35 36 37 38 5A"); |
| 325 | + round_trip(&example_bytes, 1728312610, 123_456_780); |
| 326 | + } |
| 327 | + |
| 328 | + #[test] |
| 329 | + fn round_trip_nanoseconds_123_456_789() { |
| 330 | + let example_bytes = hex!( |
| 331 | + "18 19 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 31 32 33 34 35 36 37 38 39 5A" |
| 332 | + ); |
| 333 | + round_trip(&example_bytes, 1728312610, 123_456_789); |
| 334 | + } |
| 335 | + |
| 336 | + #[test] |
| 337 | + fn round_trip_nanoseconds_000_000_005() { |
| 338 | + let example_bytes = hex!( |
| 339 | + "18 19 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 30 30 30 30 30 30 30 30 35 5A" |
| 340 | + ); |
| 341 | + round_trip(&example_bytes, 1728312610, 5); |
| 342 | + } |
| 343 | + |
| 344 | + #[test] |
| 345 | + fn invalid_generalized_time_delimiter_no_subsec() { |
| 346 | + let example_bytes = hex!("18 10 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 5A"); |
| 347 | + let err = GeneralizedTimeNanos::from_der(&example_bytes).unwrap_err(); |
| 348 | + assert_eq!( |
| 349 | + err.kind(), |
| 350 | + der::ErrorKind::Value { |
| 351 | + tag: der::Tag::GeneralizedTime |
| 352 | + } |
| 353 | + ); |
| 354 | + } |
| 355 | + |
| 356 | + #[test] |
| 357 | + fn invalid_generalized_time_trailing_zeroes() { |
| 358 | + let example_bytes = hex!( |
| 359 | + "18 1A 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 33 32 30 30 30 30 30 30 30 30 5A" |
| 360 | + ); |
| 361 | + let err = GeneralizedTimeNanos::from_der(&example_bytes).unwrap_err(); |
| 362 | + assert_eq!( |
| 363 | + err.kind(), |
| 364 | + der::ErrorKind::Value { |
| 365 | + tag: der::Tag::GeneralizedTime |
| 366 | + } |
| 367 | + ); |
| 368 | + } |
| 369 | + |
| 370 | + #[test] |
| 371 | + fn invalid_generalized_time_too_long() { |
| 372 | + let example_bytes = hex!( |
| 373 | + "18 1A 32 30 32 34 31 30 30 37 31 34 35 30 31 30 2E 30 30 30 30 30 30 30 30 30 35 5A" |
| 374 | + ); |
| 375 | + let err = GeneralizedTimeNanos::from_der(&example_bytes).unwrap_err(); |
| 376 | + assert_eq!( |
| 377 | + err.kind(), |
| 378 | + der::ErrorKind::Value { |
| 379 | + tag: der::Tag::GeneralizedTime |
| 380 | + } |
| 381 | + ); |
| 382 | + } |
| 383 | +} |
0 commit comments