@@ -52,25 +52,34 @@ pub(crate) fn get_words_from_big_int(data: BigInt) -> (bool, Vec<u64>) {
5252#[ allow( deprecated) ]
5353pub ( crate ) fn get_utc_date_time_from_asn1_milli < T : AsRef < [ u8 ] > > ( data : T ) -> Result < DateTime < Utc > > {
5454 let mut decoder = rasn:: ber:: de:: Decoder :: new ( data. as_ref ( ) , DecoderOptions :: ber ( ) ) ;
55- let ( decoded, format) = match data. as_ref ( ) . first ( ) . unwrap_or ( & 0 ) {
55+ let ( decoded, format, is_utc_time ) = match data. as_ref ( ) . first ( ) . unwrap_or ( & 0 ) {
5656 0x17 => (
5757 Utf8String :: decode_with_tag ( & mut decoder, Tag :: UTC_TIME ) ,
5858 ASN1_DATE_TIME_UTC_FORMAT ,
59+ true ,
5960 ) ,
6061 0x18 => (
6162 Utf8String :: decode_with_tag ( & mut decoder, Tag :: GENERALIZED_TIME ) ,
6263 ASN1_DATE_TIME_GENERAL_FORMAT_WITH_MS ,
64+ false ,
6365 ) ,
6466 _ => bail ! ( ASN1NAPIError :: MalformedData ) ,
6567 } ;
6668
6769 if let Ok ( decoded) = decoded {
6870 if let Some ( offset) = FixedOffset :: east_opt ( 0 ) {
69- Ok ( DateTime :: < FixedOffset > :: from_utc (
70- NaiveDateTime :: parse_from_str ( & decoded, format) ?,
71- offset,
72- )
73- . with_timezone ( & Utc ) )
71+ let mut naive = NaiveDateTime :: parse_from_str ( & decoded, format) ?;
72+
73+ // RFC 5280 §4.1.2.5.1: UTCTime pivots at 50 (>= 50 → 19xx),
74+ // but chrono's %y pivots at 70. Correct the 50-69 range.
75+ // See `<https://github.com/chronotope/chrono/issues/1152>`
76+ if is_utc_time && naive. year ( ) >= 2050 {
77+ naive = naive
78+ . with_year ( naive. year ( ) - 100 )
79+ . ok_or ( ASN1NAPIError :: MalformedData ) ?;
80+ }
81+
82+ Ok ( DateTime :: < FixedOffset > :: from_utc ( naive, offset) . with_timezone ( & Utc ) )
7483 } else {
7584 bail ! ( ASN1NAPIError :: MalformedData )
7685 }
@@ -311,7 +320,8 @@ pub(crate) fn header_length(data: &[u8]) -> Result<usize, &'static str> {
311320
312321#[ cfg( test) ]
313322mod test {
314- use chrono:: { TimeZone , Utc } ;
323+ use anyhow:: Result ;
324+ use chrono:: { Datelike , TimeZone , Utc } ;
315325 use num_bigint:: BigInt ;
316326
317327 use crate :: utils:: get_utf16_from_string;
@@ -321,56 +331,106 @@ mod test {
321331 use super :: get_utc_date_time_from_asn1_milli;
322332 use super :: get_words_from_big_int;
323333
334+ fn build_time_der ( tag : u8 , time_str : & str ) -> Vec < u8 > {
335+ let bytes = time_str. as_bytes ( ) ;
336+ let mut der = Vec :: with_capacity ( 2 + bytes. len ( ) ) ;
337+ der. push ( tag) ;
338+ der. push ( bytes. len ( ) as u8 ) ;
339+ der. extend_from_slice ( bytes) ;
340+ der
341+ }
342+
343+ fn utc_time_der ( time_str : & str ) -> Vec < u8 > {
344+ build_time_der ( 0x17 , time_str)
345+ }
346+
347+ fn generalized_time_der ( time_str : & str ) -> Vec < u8 > {
348+ build_time_der ( 0x18 , time_str)
349+ }
350+
324351 #[ test]
325352 fn test_get_utf16_from_string ( ) {
326353 assert_eq ! ( get_utf16_from_string( "test" ) , vec![ 0x74 , 0x65 , 0x73 , 0x74 ] ) ;
327354 }
328355
329356 #[ test]
330- fn test_get_oid_elements_from_string ( ) {
331- assert_eq ! (
332- get_oid_elements_from_string( "2.5.4.5" ) . unwrap( ) ,
333- vec![ 2 , 5 , 4 , 5 ]
334- ) ;
335- }
357+ fn test_oid_roundtrip ( ) -> Result < ( ) > {
358+ let cases: & [ ( & str , & [ u32 ] ) ] = & [ ( "2.5.4.5" , & [ 2 , 5 , 4 , 5 ] ) ] ;
359+
360+ for ( string, elements) in cases {
361+ assert_eq ! (
362+ get_oid_elements_from_string( string) ?,
363+ * elements,
364+ "parse {string}"
365+ ) ;
366+ assert_eq ! (
367+ get_string_from_oid_elements( * elements) ?,
368+ * string,
369+ "format {elements:?}"
370+ ) ;
371+ }
336372
337- #[ test]
338- fn test_get_string_from_oid_elements ( ) {
339- assert_eq ! (
340- get_string_from_oid_elements( [ 2 , 5 , 4 , 5 ] ) . unwrap( ) ,
341- "2.5.4.5"
342- ) ;
373+ Ok ( ( ) )
343374 }
344375
345376 #[ test]
346377 fn test_get_words_from_big_int ( ) {
347- let input = BigInt :: from ( 18591708106338011145_i128 ) ;
348- let ( negative, words) = get_words_from_big_int ( input) ;
349-
350- assert ! ( !negative) ;
351- assert_eq ! ( words, vec![ 0x203040506070809 , 0x01 ] ) ;
352-
353- let input = BigInt :: from ( -18591708106338011145_i128 ) ;
354- let ( negative, words) = get_words_from_big_int ( input) ;
378+ let cases: & [ ( i128 , bool , & [ u64 ] ) ] = & [
379+ ( 18591708106338011145 , false , & [ 0x203040506070809 , 0x01 ] ) ,
380+ ( -18591708106338011145 , true , & [ 0x203040506070809 , 0x01 ] ) ,
381+ ] ;
355382
356- assert ! ( negative) ;
357- assert_eq ! ( words, vec![ 0x203040506070809 , 0x01 ] ) ;
383+ for ( value, expected_negative, expected_words) in cases {
384+ let ( negative, words) = get_words_from_big_int ( BigInt :: from ( * value) ) ;
385+ assert_eq ! ( negative, * expected_negative, "sign for {value}" ) ;
386+ assert_eq ! ( words. as_slice( ) , * expected_words, "words for {value}" ) ;
387+ }
358388 }
359389
360390 #[ test]
361- fn test_get_utc_date_time_from_asn1_milli ( ) {
362- let date = Utc . timestamp_millis_opt ( 1655921880210 ) . unwrap ( ) ;
363- let input = [
364- 24 , 19 , 50 , 48 , 50 , 50 , 48 , 54 , 50 , 50 , 49 , 56 , 49 , 56 , 48 , 48 , 46 , 50 , 49 , 48 , 90 ,
391+ fn test_utc_time_rfc5280_pivot ( ) -> Result < ( ) > {
392+ // RFC 5280 §4.1.2.5.1: >= 50 → 19xx, < 50 → 20xx
393+ let cases: & [ ( & str , i32 ) ] = & [
394+ ( "000601120000Z" , 2000 ) , // below pivot, 21st century
395+ ( "490601120000Z" , 2049 ) , // boundary: last year below pivot
396+ ( "500601120000Z" , 1950 ) , // boundary: first year at pivot
397+ ( "690101000000Z" , 1969 ) , // above pivot, chrono disagrees (bug case)
398+ ( "700601120000Z" , 1970 ) , // at chrono's pivot, both agree
399+ ( "990601120000Z" , 1999 ) , // max 2-digit year
365400 ] ;
366401
367- assert_eq ! ( get_utc_date_time_from_asn1_milli( input) . unwrap( ) , date) ;
402+ for ( time_str, expected_year) in cases {
403+ let result = get_utc_date_time_from_asn1_milli ( utc_time_der ( time_str) ) ?;
404+ assert_eq ! (
405+ result. year( ) ,
406+ * expected_year,
407+ "UTCTime {time_str} should decode to year {expected_year}"
408+ ) ;
409+ }
410+
411+ Ok ( ( ) )
412+ }
368413
369- let date = Utc . with_ymd_and_hms ( 2022 , 9 , 26 , 10 , 0 , 0 ) . unwrap ( ) ;
370- let input = [
371- 24 , 15 , 50 , 48 , 50 , 50 , 48 , 57 , 50 , 54 , 49 , 48 , 48 , 48 , 48 , 48 , 90 ,
414+ #[ test]
415+ fn test_generalized_time_decode ( ) -> Result < ( ) > {
416+ let cases = [
417+ (
418+ "20220622181800.210Z" ,
419+ Utc . timestamp_millis_opt ( 1655921880210 ) . single ( ) ,
420+ ) ,
421+ (
422+ "20220926100000Z" ,
423+ Utc . with_ymd_and_hms ( 2022 , 9 , 26 , 10 , 0 , 0 ) . single ( ) ,
424+ ) ,
372425 ] ;
373426
374- assert_eq ! ( get_utc_date_time_from_asn1_milli( input) . unwrap( ) , date) ;
427+ for ( time_str, expected) in cases {
428+ let expected =
429+ expected. ok_or_else ( || anyhow:: anyhow!( "invalid test date {time_str}" ) ) ?;
430+ let result = get_utc_date_time_from_asn1_milli ( generalized_time_der ( time_str) ) ?;
431+ assert_eq ! ( result, expected, "GeneralizedTime {time_str}" ) ;
432+ }
433+
434+ Ok ( ( ) )
375435 }
376436}
0 commit comments