Skip to content

Commit 848329f

Browse files
committed
Refactor ASN1_Time and tweak rules on ASN.1 date formats accepted
RFC 5280 is silent on the issue of leap seconds, but both Go and OpenSSL reject them entirely. Given that, it seems safe to assume they are not used anywhere in practice. Previously leap seconds were accepted for GeneralizedTime. Previously years past 3100 were rejected. However this prevented parsing certificates which use RFC 5280's "no well-defined expiration date" of 99991231235959Z. Extend this so that years up to 9999 are accepted. Skip gmtime thread-safety gymnastics and use Howard Hinnant's civil_from_days algorithm. Change types in ASN1_Time and calendar_point to not waste storage.
1 parent 625633f commit 848329f

10 files changed

Lines changed: 370 additions & 224 deletions

File tree

src/cli/roughtime.cpp

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,22 @@
1616

1717
#include <fstream>
1818
#include <iomanip>
19+
#include <sstream>
1920

2021
namespace Botan_CLI {
2122

2223
namespace {
2324

25+
// Format the time point as YYYY-MM-DDTHH:MM:SS in UTC
26+
std::string format_utc_datetime(const std::chrono::system_clock::time_point& tp) {
27+
const Botan::calendar_point c(tp);
28+
std::ostringstream out;
29+
out << std::setfill('0') << std::setw(4) << c.year() << "-" << std::setw(2) << c.month() << "-" << std::setw(2)
30+
<< c.day() << "T" << std::setw(2) << c.hour() << ":" << std::setw(2) << c.minutes() << ":" << std::setw(2)
31+
<< c.seconds();
32+
return out.str();
33+
}
34+
2435
class RoughtimeCheck final : public Command {
2536
public:
2637
RoughtimeCheck() : Command("roughtime_check --raw-time chain-file") {}
@@ -38,7 +49,7 @@ class RoughtimeCheck final : public Command {
3849
output()
3950
<< Botan::Roughtime::Response::sys_microseconds64(response.utc_midpoint()).time_since_epoch().count();
4051
} else {
41-
output() << Botan::calendar_point(response.utc_midpoint()).to_string();
52+
output() << format_utc_datetime(response.utc_midpoint());
4253
}
4354
output() << " (+-" << Botan::Roughtime::Response::microseconds32(response.utc_radius()).count() << "us)\n";
4455
}
@@ -100,7 +111,7 @@ class Roughtime final : public Command {
100111
<< "UTC "
101112
<< Botan::Roughtime::Response::sys_microseconds64(response.utc_midpoint()).time_since_epoch().count();
102113
} else {
103-
output() << "UTC " << Botan::calendar_point(response.utc_midpoint()).to_string();
114+
output() << "UTC " << format_utc_datetime(response.utc_midpoint());
104115
}
105116
output() << " (+-" << Botan::Roughtime::Response::microseconds32(response.utc_radius()).count() << "us)";
106117
if(!response.validate(public_key)) {

src/lib/asn1/asn1_time.cpp

Lines changed: 150 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -14,37 +14,146 @@
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

1919
namespace 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+
2137
ASN1_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

109218
std::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

123233
bool 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-
267286
std::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

Comments
 (0)