Skip to content

Commit adf0cf6

Browse files
authored
Merge pull request #5668 from randombit/jack/asn1-time-refactor
Refactor ASN1_Time and tweak rules on ASN.1 date formats accepted
2 parents 1d3b4ab + 848329f commit adf0cf6

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)