Skip to content

Commit b6cbf59

Browse files
Time point parsing bounds (#7648)
Co-authored-by: Amaury Chamayou <amchamay@microsoft.com> Co-authored-by: Amaury Chamayou <amaury@xargs.fr>
1 parent 2e93a0c commit b6cbf59

9 files changed

Lines changed: 219 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1616
### Fixed
1717

1818
- Only rollback uncommittable indices during become_leader (#7620)
19+
- x509 parsing now correctly handles times validity beyond 2262. To support this, some public function signatures (`ccf::ds::time_point_from_string()`, `ccf::crypto::Verifier::remaining_seconds()`) now use `time_point`s from `ccf::nonstd::SystemClock` rather than `std::chrono::system_clock` (#7648)
1920

2021
## [7.0.0-dev9]
2122

include/ccf/crypto/openssl/openssl_wrappers.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ namespace ccf::crypto::OpenSSL
359359
Unique_X509_TIME(ASN1_TIME* t) :
360360
Unique_SSL_OBJECT(t, ASN1_TIME_free, /*check_null=*/false)
361361
{}
362-
Unique_X509_TIME(const std::chrono::system_clock::time_point& t) :
362+
Unique_X509_TIME(const ccf::nonstd::SystemClock::time_point& t) :
363363
Unique_X509_TIME(ccf::ds::to_x509_time_string(t))
364364
{}
365365
};

include/ccf/crypto/verifier.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "ccf/crypto/jwk.h"
77
#include "ccf/crypto/pem.h"
88
#include "ccf/crypto/rsa_public_key.h"
9+
#include "ccf/ds/nonstd.h"
910

1011
#include <chrono>
1112

@@ -154,11 +155,11 @@ namespace ccf::crypto
154155
/** The number of seconds of the validity period of the
155156
* certificate remaining */
156157
[[nodiscard]] virtual size_t remaining_seconds(
157-
const std::chrono::system_clock::time_point& now) const = 0;
158+
const ccf::nonstd::SystemClock::time_point& now) const = 0;
158159

159160
/** The percentage of the validity period of the certificate remaining */
160161
[[nodiscard]] virtual double remaining_percentage(
161-
const std::chrono::system_clock::time_point& now) const = 0;
162+
const ccf::nonstd::SystemClock::time_point& now) const = 0;
162163

163164
/** The subject name of the certificate */
164165
[[nodiscard]] virtual std::string subject() const = 0;

include/ccf/ds/nonstd.h

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
#include <array>
66
#include <cctype>
7+
#include <chrono>
8+
#include <ctime>
79
#include <regex>
810
#include <string>
911
#include <string_view>
@@ -210,9 +212,38 @@ namespace ccf::nonstd
210212
*fd = -1;
211213
}
212214
}
215+
213216
using CloseFdGuard = std::unique_ptr<int, decltype(&close_fd)>;
214217
static inline CloseFdGuard make_close_fd_guard(int* fd)
215218
{
216219
return {fd, close_fd};
217220
}
221+
222+
// A custom clock type for handling certificate validity periods, which are
223+
// defined in terms of seconds since the epoch. This avoids issues with
224+
// system_clock::time_point being unable to represent times after 2262-04-11
225+
// 23:47:17 UTC (due to tracking nanosecond precision).
226+
struct SystemClock
227+
{
228+
using duration = std::chrono::seconds;
229+
using rep = duration::rep;
230+
using period = duration::period;
231+
using time_point = std::chrono::time_point<SystemClock>;
232+
static constexpr bool is_steady = false;
233+
234+
static time_point now() noexcept
235+
{
236+
return time_point(duration(std::time(nullptr)));
237+
}
238+
239+
static std::time_t to_time_t(const time_point& t) noexcept
240+
{
241+
return std::time_t(t.time_since_epoch().count());
242+
}
243+
244+
static time_point from_time_t(std::time_t t) noexcept
245+
{
246+
return time_point(duration(t));
247+
}
248+
};
218249
}

include/ccf/ds/x509_time_fmt.h

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// Licensed under the Apache 2.0 License.
33
#pragma once
44

5-
#define FMT_HEADER_ONLY
5+
#include "ccf/ds/nonstd.h"
6+
67
#include <chrono>
8+
#define FMT_HEADER_ONLY
79
#include <fmt/chrono.h>
810
#include <fmt/format.h>
911
#include <iomanip>
@@ -20,13 +22,20 @@ namespace ccf::ds
2022
return fmt::format("{:%Y%m%d%H%M%SZ}", time);
2123
}
2224

25+
static inline std::string to_x509_time_string(
26+
const ccf::nonstd::SystemClock::time_point& time)
27+
{
28+
return to_x509_time_string(
29+
fmt::gmtime(ccf::nonstd::SystemClock::to_time_t(time)));
30+
}
31+
2332
static inline std::string to_x509_time_string(
2433
const std::chrono::system_clock::time_point& time)
2534
{
2635
return to_x509_time_string(fmt::gmtime(time));
2736
}
2837

29-
static inline std::chrono::system_clock::time_point time_point_from_string(
38+
static inline ccf::nonstd::SystemClock::time_point time_point_from_string(
3039
const std::string& time)
3140
{
3241
// NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers)
@@ -44,7 +53,7 @@ namespace ccf::ds
4453
auto* sres = strptime(ts, afmt, &t);
4554
if (sres != nullptr && *sres == '\0')
4655
{
47-
auto r = std::chrono::system_clock::from_time_t(timegm(&t));
56+
auto r = ccf::nonstd::SystemClock::from_time_t(timegm(&t));
4857
r -= std::chrono::seconds(t.tm_gmtoff);
4958
return r;
5059
}
@@ -56,8 +65,8 @@ namespace ccf::ds
5665
{"%04u-%02u-%02u %02u:%02u:%f %d:%02u", 8},
5766
{"%04u-%02u-%02uT%02u:%02u:%f %d:%02u", 8},
5867
{"%04u-%02u-%02u %02u:%02u:%f %03d %02u", 8},
59-
{"%02u%02u%02u%02u%02u%02f%03d%02u", 8},
60-
{"%04u%02u%02u%02u%02u%02f%03d%02u", 8},
68+
{"%02u%02u%02u%02u%02u%f%03d%02u", 8},
69+
{"%04u%02u%02u%02u%02u%f%03d%02u", 8},
6170
{"%04u-%02u-%02uT%02u:%02u:%f", 6},
6271
{"%04u-%02u-%02u %02u:%02u:%f", 6}};
6372

@@ -76,7 +85,6 @@ namespace ccf::ds
7685
if (rs >= 1 && rs == n)
7786
{
7887
using namespace std::chrono;
79-
8088
if (strncmp(fmt, "%02u", 4) == 0)
8189
{
8290
// ASN.1 two-digit year range
@@ -94,16 +102,30 @@ namespace ccf::ds
94102
continue;
95103
}
96104

97-
system_clock::time_point r = (sys_days)date;
105+
// Build a struct tm and use timegm() to convert to time_t
106+
// directly, avoiding system_clock::time_point which can
107+
// overflow for dates outside ~1677-2262.
108+
struct tm t = {};
109+
t.tm_year = static_cast<int>(y) - 1900;
110+
t.tm_mon = static_cast<int>(m) - 1;
111+
t.tm_mday = static_cast<int>(d);
98112
if (rs >= 6)
99113
{
100-
r += hours(h) + minutes(mn) + microseconds((long)(s * 1e6));
114+
t.tm_hour = static_cast<int>(h);
115+
t.tm_min = static_cast<int>(mn);
116+
t.tm_sec = static_cast<int>(s);
101117
}
118+
119+
auto tt = timegm(&t);
120+
102121
if (rs >= 8)
103122
{
104-
r -= hours(oh) + minutes(om);
123+
auto offset_secs = oh * 3600 +
124+
(oh < 0 ? -static_cast<int>(om) : static_cast<int>(om)) * 60;
125+
tt -= offset_secs;
105126
}
106-
return r;
127+
128+
return ccf::nonstd::SystemClock::from_time_t(tt);
107129
}
108130
}
109131
}

src/crypto/openssl/verifier.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ namespace ccf::crypto
207207
}
208208

209209
size_t Verifier_OpenSSL::remaining_seconds(
210-
const std::chrono::system_clock::time_point& now) const
210+
const ccf::nonstd::SystemClock::time_point& now) const
211211
{
212212
auto [from, to] = validity_period();
213213
auto tp_to = ccf::ds::time_point_from_string(to);
@@ -217,7 +217,7 @@ namespace ccf::crypto
217217
}
218218

219219
double Verifier_OpenSSL::remaining_percentage(
220-
const std::chrono::system_clock::time_point& now) const
220+
const ccf::nonstd::SystemClock::time_point& now) const
221221
{
222222
auto [from, to] = validity_period();
223223
auto tp_from = ccf::ds::time_point_from_string(from);

src/crypto/openssl/verifier.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,10 @@ namespace ccf::crypto
3838
std::pair<std::string, std::string> validity_period() const override;
3939

4040
size_t remaining_seconds(
41-
const std::chrono::system_clock::time_point& now) const override;
41+
const ccf::nonstd::SystemClock::time_point& now) const override;
4242

4343
double remaining_percentage(
44-
const std::chrono::system_clock::time_point& now) const override;
44+
const ccf::nonstd::SystemClock::time_point& now) const override;
4545

4646
std::string subject() const override;
4747
};

src/crypto/test/crypto.cpp

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ ccf::crypto::Pem generate_self_signed_cert(
191191
constexpr size_t certificate_validity_period_days = 365;
192192
using namespace std::literals;
193193
auto valid_from =
194-
ccf::ds::to_x509_time_string(std::chrono::system_clock::now() - 24h);
194+
ccf::ds::to_x509_time_string(ccf::nonstd::SystemClock::now() - 24h);
195195

196196
return ccf::crypto::create_self_signed_cert(
197197
kp, name, {}, valid_from, certificate_validity_period_days);
@@ -750,6 +750,11 @@ TEST_CASE("Non-ASN.1 timepoint formats")
750750
conv = ccf::ds::to_x509_time_string(tp);
751751
REQUIRE(conv == "20220405215327Z");
752752

753+
time_str = "2026-02-09 05:00:00 -03:30";
754+
tp = ccf::ds::time_point_from_string(time_str);
755+
conv = ccf::ds::to_x509_time_string(tp);
756+
REQUIRE(conv == "20260209083000Z");
757+
753758
time_str = "2022-04-07T10:37:49.567612";
754759
tp = ccf::ds::time_point_from_string(time_str);
755760
conv = ccf::ds::to_x509_time_string(tp);
@@ -781,6 +786,91 @@ TEST_CASE("Non-ASN.1 timepoint formats")
781786
REQUIRE(conv == "20220425195619Z");
782787
}
783788

789+
TEST_CASE("Timepoint bounds")
790+
{
791+
// Can handle values beyond bounds of system_clock::time_point
792+
{
793+
INFO("Beyond system_clock::time_point min");
794+
auto time_str = "1677-09-21 00:12:44";
795+
auto tp = ccf::ds::time_point_from_string(time_str);
796+
auto conv = ccf::ds::to_x509_time_string(tp);
797+
REQUIRE(conv == "16770921001244Z");
798+
799+
time_str = "1677-09-21 00:12:43";
800+
tp = ccf::ds::time_point_from_string(time_str);
801+
conv = ccf::ds::to_x509_time_string(tp);
802+
REQUIRE(conv == "16770921001243Z");
803+
}
804+
805+
{
806+
INFO("Beyond system_clock::time_point max");
807+
auto time_str = "2262-04-11 23:47:16";
808+
auto tp = ccf::ds::time_point_from_string(time_str);
809+
auto conv = ccf::ds::to_x509_time_string(tp);
810+
REQUIRE(conv == "22620411234716Z");
811+
812+
time_str = "2262-04-11 23:47:17";
813+
tp = ccf::ds::time_point_from_string(time_str);
814+
conv = ccf::ds::to_x509_time_string(tp);
815+
CHECK(conv == "22620411234717Z");
816+
}
817+
818+
{
819+
INFO("Approx ASN.1 format bounds");
820+
auto time_str = "9999-12-31 23:59:59";
821+
auto tp = ccf::ds::time_point_from_string(time_str);
822+
auto conv = ccf::ds::to_x509_time_string(tp);
823+
REQUIRE(conv == "99991231235959Z");
824+
825+
INFO("sscanf variants of near-min value");
826+
for (auto time_str : {
827+
"0001-02-03 04:05:06",
828+
"0001-02-03 04:05:06.700000 +0:00",
829+
"0001-02-03 12:14:06.700000 +8:09",
830+
"0001-02-02 19:56:06.700000 -8:09",
831+
832+
"0001-02-03T04:05:06.700000 +0:00",
833+
"0001-02-03T12:14:06.700000 +8:09",
834+
"0001-02-02T19:56:06.700000 -8:09",
835+
836+
"0001-02-03 04:05:06.700000 +00 00",
837+
"0001-02-03 12:14:06.700000 +08:09",
838+
"0001-02-02 19:56:06.700000 -08:09",
839+
840+
"00010203040506.700000+0000",
841+
"00010203121406.700000+0809",
842+
"00010202195606.700000-0809",
843+
844+
"0001-02-03T04:05:06.700000",
845+
"0001-02-03 04:05:06.700000",
846+
})
847+
{
848+
tp = ccf::ds::time_point_from_string(time_str);
849+
conv = ccf::ds::to_x509_time_string(tp);
850+
CHECK(conv == "00010203040506Z");
851+
}
852+
}
853+
}
854+
855+
TEST_CASE("Invalid times")
856+
{
857+
REQUIRE_THROWS_WITH(
858+
ccf::ds::time_point_from_string("hello"),
859+
"'hello' does not match any accepted time format");
860+
861+
REQUIRE_THROWS_WITH(
862+
ccf::ds::time_point_from_string("Monday"),
863+
"'Monday' does not match any accepted time format");
864+
865+
REQUIRE_THROWS_WITH(
866+
ccf::ds::time_point_from_string("April 1st, 1984"),
867+
"'April 1st, 1984' does not match any accepted time format");
868+
869+
REQUIRE_THROWS_WITH(
870+
ccf::ds::time_point_from_string("1111-1111"),
871+
"'1111-1111' does not match any accepted time format");
872+
}
873+
784874
TEST_CASE("Create sign and verify certificates")
785875
{
786876
bool corrupt_csr = false;
@@ -885,7 +975,7 @@ TEST_CASE("AES-GCM convenience functions")
885975

886976
TEST_CASE("x509 time")
887977
{
888-
auto time = std::chrono::system_clock::now();
978+
auto time = ccf::nonstd::SystemClock::now();
889979

890980
auto next_minute_time = time + 1min;
891981
auto next_day_time = time + 24h;
@@ -897,8 +987,8 @@ TEST_CASE("x509 time")
897987
{
898988
struct Input
899989
{
900-
std::chrono::system_clock::time_point from;
901-
std::chrono::system_clock::time_point to;
990+
ccf::nonstd::SystemClock::time_point from;
991+
ccf::nonstd::SystemClock::time_point to;
902992
std::optional<uint32_t> maximum_validity_period_days = std::nullopt;
903993
};
904994
Input input;
@@ -933,7 +1023,7 @@ TEST_CASE("x509 time")
9331023

9341024
INFO("Adjust time");
9351025
{
936-
std::vector<std::chrono::system_clock::time_point> times = {
1026+
std::vector<ccf::nonstd::SystemClock::time_point> times = {
9371027
time, next_day_time, next_day_time};
9381028
size_t days_offset = 100;
9391029

@@ -1415,7 +1505,7 @@ TEST_CASE("Do not trust non-ca certs")
14151505
constexpr size_t certificate_validity_period_days = 365;
14161506
using namespace std::literals;
14171507
auto valid_from =
1418-
ccf::ds::to_x509_time_string(std::chrono::system_clock::now() - 24h);
1508+
ccf::ds::to_x509_time_string(ccf::nonstd::SystemClock::now() - 24h);
14191509
auto valid_to = compute_cert_valid_to_string(
14201510
valid_from, certificate_validity_period_days);
14211511
std::vector<SubjectAltName> subject_alt_names = {};

0 commit comments

Comments
 (0)