1414#include < botan/internal/calendar.h>
1515#include < botan/internal/fmt.h>
1616#include < botan/internal/parsing.h>
17- #include < iomanip >
17+ #include < sstream >
1818
1919namespace Botan {
2020
21+ namespace {
22+
23+ // Format an integer as exactly `digits` zero-padded decimal digits
24+ std::string zero_pad (uint32_t value, size_t digits) {
25+ std::string s = std::to_string (value);
26+ BOTAN_ASSERT_NOMSG (s.size () <= digits);
27+ const size_t padding = digits - s.size ();
28+ if (padding == 0 ) {
29+ return s;
30+ } else {
31+ return std::string (padding, ' 0' ) + s;
32+ }
33+ }
34+
35+ } // namespace
36+
2137ASN1_Time ASN1_Time::from_seconds_since_epoch (uint64_t time_since_epoch) {
22- return ASN1_Time (std::chrono::system_clock::time_point (std::chrono::seconds (time_since_epoch)));
38+ return ASN1_Time::from_time_point (std::chrono::system_clock::time_point (std::chrono::seconds (time_since_epoch)));
39+ }
40+
41+ ASN1_Time::ASN1_Time (
42+ uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second, ASN1_Type tag) :
43+ m_year (year), m_month(month), m_day(day), m_hour(hour), m_minute(minute), m_second(second), m_tag(tag) {
44+ if (tag != ASN1_Type::UtcTime && tag != ASN1_Type::GeneralizedTime) {
45+ throw Invalid_Argument (" ASN1_Time tag must be UtcTime or GeneralizedTime" );
46+ }
47+
48+ /*
49+ * RFC 5280 Section 4.1.2.5:
50+ * To indicate that a certificate has no well-defined expiration date,
51+ * the notAfter SHOULD be assigned the GeneralizedTime value of
52+ * 99991231235959Z.
53+ */
54+ const uint16_t min_year = 1950 ;
55+ const uint16_t max_year = (tag == ASN1_Type::UtcTime) ? 2049 : 9999 ;
56+
57+ if (m_year < min_year || m_year > max_year) {
58+ throw Invalid_Argument (fmt (" ASN1_Time year {} is out of range ({} to {})" , m_year, min_year, max_year));
59+ }
60+
61+ if (m_month < 1 || m_month > 12 ) {
62+ throw Invalid_Argument (fmt (" ASN1_Time month {} is out of range" , static_cast <uint32_t >(m_month)));
63+ }
64+
65+ constexpr uint8_t days_in_month[12 ] = {31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 };
66+
67+ const bool is_leap_year = (m_year % 4 == 0 ) && (m_year % 100 != 0 || m_year % 400 == 0 );
68+ const uint8_t max_day = (m_month == 2 && is_leap_year) ? 29 : days_in_month[m_month - 1 ];
69+
70+ if (m_day < 1 || m_day > max_day) {
71+ throw Invalid_Argument (fmt (" ASN1_Time day {} is out of range for month {}" ,
72+ static_cast <uint32_t >(m_day),
73+ static_cast <uint32_t >(m_month)));
74+ }
75+
76+ if (m_hour > 23 ) {
77+ throw Invalid_Argument (fmt (" ASN1_Time hour {} is out of range" , static_cast <uint32_t >(m_hour)));
78+ }
79+
80+ if (m_minute > 59 ) {
81+ throw Invalid_Argument (fmt (" ASN1_Time minute {} is out of range" , static_cast <uint32_t >(m_minute)));
82+ }
83+
84+ /*
85+ * RFC 5280 is silent on the issue of leap seconds in certificate fields, but both
86+ * OpenSSL and Go reject which suggests they are rarely if ever used in practice.
87+ */
88+ if (m_second > 59 ) {
89+ throw Invalid_Argument (fmt (" ASN1_Time second {} is out of range" , static_cast <uint32_t >(m_second)));
90+ }
2391}
2492
25- ASN1_Time::ASN1_Time (const std::chrono::system_clock::time_point& time) {
93+ // static
94+ ASN1_Time ASN1_Time::from_time_point (const std::chrono::system_clock::time_point& time) {
2695 const calendar_point cal (time);
2796
28- m_year = cal.year ();
29- m_month = cal.month ();
30- m_day = cal.day ();
31- m_hour = cal.hour ();
32- m_minute = cal.minutes ();
33- m_second = cal.seconds ();
97+ const ASN1_Type tag = (cal.year () >= 2050 ) ? ASN1_Type::GeneralizedTime : ASN1_Type::UtcTime;
3498
35- // NOLINTNEXTLINE(*-prefer-member-initializer)
36- m_tag = (m_year >= 2050 ) ? ASN1_Type::GeneralizedTime : ASN1_Type::UtcTime;
99+ return ASN1_Time (static_cast <uint16_t >(cal.year ()),
100+ static_cast <uint8_t >(cal.month ()),
101+ static_cast <uint8_t >(cal.day ()),
102+ static_cast <uint8_t >(cal.hour ()),
103+ static_cast <uint8_t >(cal.minutes ()),
104+ static_cast <uint8_t >(cal.seconds ()),
105+ tag);
37106}
38107
39- ASN1_Time::ASN1_Time (std::string_view t_spec, ASN1_Type tag) {
40- set_to (t_spec, tag);
108+ // static
109+ ASN1_Time ASN1_Time::from_string (std::string_view t_spec, ASN1_Type tag) {
110+ BOTAN_ARG_CHECK (tag == ASN1_Type::UtcTime || tag == ASN1_Type::GeneralizedTime, " Invalid tag for ASN1_Time" );
111+
112+ if (tag == ASN1_Type::GeneralizedTime) {
113+ BOTAN_ARG_CHECK (t_spec.size () == 15 , " Invalid GeneralizedTime input string" );
114+ } else {
115+ BOTAN_ARG_CHECK (t_spec.size () == 13 , " Invalid UTCTime input string" );
116+ }
117+
118+ BOTAN_ARG_CHECK (t_spec.back () == ' Z' , " Botan does not support ASN1 times with timezones other than Z" );
119+
120+ const size_t field_len = 2 ;
121+ const size_t year_len = (tag == ASN1_Type::UtcTime) ? 2 : 4 ;
122+
123+ const size_t year_start = 0 ;
124+ const size_t month_start = year_start + year_len;
125+ const size_t day_start = month_start + field_len;
126+ const size_t hour_start = day_start + field_len;
127+ const size_t min_start = hour_start + field_len;
128+ const size_t sec_start = min_start + field_len;
129+
130+ uint32_t year = to_u32bit (t_spec.substr (year_start, year_len));
131+ const uint32_t month = to_u32bit (t_spec.substr (month_start, field_len));
132+ const uint32_t day = to_u32bit (t_spec.substr (day_start, field_len));
133+ const uint32_t hour = to_u32bit (t_spec.substr (hour_start, field_len));
134+ const uint32_t minute = to_u32bit (t_spec.substr (min_start, field_len));
135+ const uint32_t second = to_u32bit (t_spec.substr (sec_start, field_len));
136+
137+ if (tag == ASN1_Type::UtcTime) {
138+ // Interpret the two digit year by the 1950/2050 split (RFC 5280 Section 4.1.2.5.1)
139+ year += (year >= 50 ) ? 1900 : 2000 ;
140+ }
141+
142+ return ASN1_Time (static_cast <uint16_t >(year),
143+ static_cast <uint8_t >(month),
144+ static_cast <uint8_t >(day),
145+ static_cast <uint8_t >(hour),
146+ static_cast <uint8_t >(minute),
147+ static_cast <uint8_t >(second),
148+ tag);
41149}
42150
43- ASN1_Time::ASN1_Time (std::string_view t_spec) {
151+ // static
152+ ASN1_Time ASN1_Time::from_string (std::string_view t_spec) {
44153 if (t_spec.size () == 13 ) {
45- set_to (t_spec, ASN1_Type::UtcTime);
154+ return ASN1_Time::from_string (t_spec, ASN1_Type::UtcTime);
46155 } else if (t_spec.size () == 15 ) {
47- set_to (t_spec, ASN1_Type::GeneralizedTime);
156+ return ASN1_Time::from_string (t_spec, ASN1_Type::GeneralizedTime);
48157 } else {
49158 throw Invalid_Argument (" Time string could not be parsed as GeneralizedTime or UTCTime." );
50159 }
@@ -67,7 +176,9 @@ void ASN1_Time::decode_from(BER_Decoder& source) {
67176 }
68177
69178 try {
70- set_to (ASN1::to_string (ber_time), ber_time.type ());
179+ // Assigning only after a successful parse means that a decoding error
180+ // cannot leave this object in a partially written state
181+ *this = ASN1_Time::from_string (ASN1::to_string (ber_time), ber_time.type ());
71182 } catch (Invalid_Argument& e) {
72183 throw Decoding_Error (fmt (" Invalid ASN1_Time encoding: {}" , e.what ()));
73184 }
@@ -78,46 +189,45 @@ std::string ASN1_Time::to_string() const {
78189 throw Invalid_State (" ASN1_Time::to_string: No time set" );
79190 }
80191
81- uint32_t full_year = m_year;
192+ BOTAN_ASSERT_NOMSG (m_year <= 9999 );
193+
194+ std::ostringstream out;
82195
196+ // UTCTime uses a 2 digit year, GeneralizedTime a 4 digit year
83197 if (m_tag == ASN1_Type::UtcTime) {
84198 if (m_year < 1950 || m_year >= 2050 ) {
85199 throw Encoding_Error (fmt (" ASN_Time: The time {} cannot be encoded as UTCTime" , readable_string ()));
86200 }
87201
88- full_year = (m_year >= 2000 ) ? (m_year - 2000 ) : (m_year - 1900 );
202+ out << (zero_pad ((m_year >= 2000 ) ? (m_year - 2000 ) : (m_year - 1900 ), 2 ));
203+ } else {
204+ out << zero_pad (m_year, 4 );
89205 }
90206
91- const uint64_t year_factor = 10000000000 ;
92- const uint64_t mon_factor = 100000000 ;
93- const uint64_t day_factor = 1000000 ;
94- const uint64_t hour_factor = 10000 ;
95- const uint64_t min_factor = 100 ;
96-
97- const uint64_t int_repr = year_factor * full_year + mon_factor * m_month + day_factor * m_day +
98- hour_factor * m_hour + min_factor * m_minute + m_second;
99-
100- const std::string repr = std::to_string (int_repr) + " Z" ;
207+ // clang-format off
208+ out << zero_pad (m_month, 2 )
209+ << zero_pad (m_day, 2 )
210+ << zero_pad (m_hour, 2 )
211+ << zero_pad (m_minute, 2 )
212+ << zero_pad (m_second, 2 ) << " Z" ;
213+ // clang-format on
101214
102- const size_t desired_size = (m_tag == ASN1_Type::UtcTime) ? 13 : 15 ;
103-
104- const std::string zero_padding (desired_size - repr.size (), ' 0' );
105-
106- return zero_padding + repr;
215+ return out.str ();
107216}
108217
109218std::string ASN1_Time::readable_string () const {
110219 if (!time_is_set ()) {
111220 throw Invalid_State (" ASN1_Time::readable_string: No time set" );
112221 }
113222
114- // desired format: "%04d/%02d/%02d %02d:%02d:%02d UTC"
115- std::stringstream output;
116- output << std::setfill (' 0' ) << std::setw (4 ) << m_year << " /" << std::setw (2 ) << m_month << " /" << std::setw (2 )
117- << m_day << " " << std::setw (2 ) << m_hour << " :" << std::setw (2 ) << m_minute << " :" << std::setw (2 )
118- << m_second << " UTC" ;
223+ // desired format: "YYYY/MM/DD HH:MM:SS UTC"
224+
225+ std::ostringstream out;
119226
120- return output.str ();
227+ out << zero_pad (m_year, 4 ) << " /" << zero_pad (m_month, 2 ) << " /" << zero_pad (m_day, 2 ) << " " ;
228+ out << zero_pad (m_hour, 2 ) << " :" << zero_pad (m_minute, 2 ) << " :" << zero_pad (m_second, 2 ) << " UTC" ;
229+
230+ return out.str ();
121231}
122232
123233bool ASN1_Time::time_is_set () const {
@@ -173,97 +283,6 @@ int32_t ASN1_Time::cmp(const ASN1_Time& other) const {
173283 return SAME_TIME ;
174284}
175285
176- void ASN1_Time::set_to (std::string_view t_spec, ASN1_Type spec_tag) {
177- BOTAN_ARG_CHECK (spec_tag == ASN1_Type::UtcTime || spec_tag == ASN1_Type::GeneralizedTime,
178- " Invalid tag for ASN1_Time" );
179-
180- if (spec_tag == ASN1_Type::GeneralizedTime) {
181- BOTAN_ARG_CHECK (t_spec.size () == 15 , " Invalid GeneralizedTime input string" );
182- } else if (spec_tag == ASN1_Type::UtcTime) {
183- BOTAN_ARG_CHECK (t_spec.size () == 13 , " Invalid UTCTime input string" );
184- }
185-
186- BOTAN_ARG_CHECK (t_spec.back () == ' Z' , " Botan does not support ASN1 times with timezones other than Z" );
187-
188- const size_t field_len = 2 ;
189-
190- const size_t year_start = 0 ;
191- const size_t year_len = (spec_tag == ASN1_Type::UtcTime) ? 2 : 4 ;
192- const size_t month_start = year_start + year_len;
193- const size_t day_start = month_start + field_len;
194- const size_t hour_start = day_start + field_len;
195- const size_t min_start = hour_start + field_len;
196- const size_t sec_start = min_start + field_len;
197-
198- m_year = to_u32bit (t_spec.substr (year_start, year_len));
199- m_month = to_u32bit (t_spec.substr (month_start, field_len));
200- m_day = to_u32bit (t_spec.substr (day_start, field_len));
201- m_hour = to_u32bit (t_spec.substr (hour_start, field_len));
202- m_minute = to_u32bit (t_spec.substr (min_start, field_len));
203- m_second = to_u32bit (t_spec.substr (sec_start, field_len));
204- m_tag = spec_tag;
205-
206- if (spec_tag == ASN1_Type::UtcTime) {
207- if (m_year >= 50 ) {
208- m_year += 1900 ;
209- } else {
210- m_year += 2000 ;
211- }
212- }
213-
214- if (!passes_sanity_check ()) {
215- throw Invalid_Argument (fmt (" ASN1_Time string '{}' does not seem to be valid" , t_spec));
216- }
217- }
218-
219- /*
220- * Do a general sanity check on the time
221- */
222- bool ASN1_Time::passes_sanity_check () const {
223- // AppVeyor's trust store includes a cert with expiration date in 3016 ...
224- if (m_year < 1950 || m_year > 3100 ) {
225- return false ;
226- }
227- if (m_month == 0 || m_month > 12 ) {
228- return false ;
229- }
230-
231- const uint32_t days_in_month[12 ] = {31 , 28 + 1 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 };
232-
233- if (m_day == 0 || m_day > days_in_month[m_month - 1 ]) {
234- return false ;
235- }
236-
237- if (m_month == 2 && m_day == 29 ) {
238- if (m_year % 4 != 0 ) {
239- return false ; // not a leap year
240- }
241-
242- if (m_year % 100 == 0 && m_year % 400 != 0 ) {
243- return false ;
244- }
245- }
246-
247- if (m_hour >= 24 || m_minute >= 60 || m_second > 60 ) {
248- return false ;
249- }
250-
251- if (m_tag == ASN1_Type::UtcTime) {
252- /*
253- UTCTime limits the value of components such that leap seconds
254- are not covered. See "UNIVERSAL 23" in "Information technology
255- Abstract Syntax Notation One (ASN.1): Specification of basic notation"
256-
257- http://www.itu.int/ITU-T/studygroups/com17/languages/
258- */
259- if (m_second > 59 ) {
260- return false ;
261- }
262- }
263-
264- return true ;
265- }
266-
267286std::chrono::system_clock::time_point ASN1_Time::to_std_timepoint () const {
268287 return calendar_point (m_year, m_month, m_day, m_hour, m_minute, m_second).to_std_timepoint ();
269288}
0 commit comments