From 9e9e27f742035217e6da403253a7754b79f101a7 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 6 Feb 2026 14:00:23 +0000 Subject: [PATCH 01/17] Test parsing of edge-of-bounds time points --- src/crypto/test/crypto.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index e997975e46f..dc543f2ac0d 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -781,6 +781,29 @@ TEST_CASE("Non-ASN.1 timepoint formats") REQUIRE(conv == "20220425195619Z"); } +TEST_CASE("Timepoint bounds") +{ + auto time_str = "1677-09-21 00:12:44"; + auto tp = ccf::ds::time_point_from_string(time_str); + auto conv = ccf::ds::to_x509_time_string(tp); + REQUIRE(conv == "16770921001244Z"); + + time_str = "1677-09-21 00:12:43"; + tp = ccf::ds::time_point_from_string(time_str); + conv = ccf::ds::to_x509_time_string(tp); + CHECK(conv == "16770921001243Z"); + + time_str = "2262-04-11 23:47:16"; + tp = ccf::ds::time_point_from_string(time_str); + conv = ccf::ds::to_x509_time_string(tp); + REQUIRE(conv == "22620411234716Z"); + + time_str = "2262-04-11 23:47:17"; + tp = ccf::ds::time_point_from_string(time_str); + conv = ccf::ds::to_x509_time_string(tp); + CHECK(conv == "22620411234717Z"); +} + TEST_CASE("Create sign and verify certificates") { bool corrupt_csr = false; From aba6a887fd1204fd23a685a0b5c9aede15b03503 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 6 Feb 2026 15:26:07 +0000 Subject: [PATCH 02/17] More crypto test --- src/crypto/test/crypto.cpp | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index dc543f2ac0d..c51c270d8cc 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -789,8 +789,12 @@ TEST_CASE("Timepoint bounds") REQUIRE(conv == "16770921001244Z"); time_str = "1677-09-21 00:12:43"; - tp = ccf::ds::time_point_from_string(time_str); - conv = ccf::ds::to_x509_time_string(tp); + REQUIRE_THROWS_WITH( + ccf::ds::time_point_from_string(time_str), + "'1677-09-21 00:12:43' is too far in the past to be represented as a " + "system_clock::time_point"); + auto s = ccf::ds::since_epoch_from_string(time_str); + conv = ccf::ds::to_x509_time_string(s); CHECK(conv == "16770921001243Z"); time_str = "2262-04-11 23:47:16"; @@ -799,11 +803,34 @@ TEST_CASE("Timepoint bounds") REQUIRE(conv == "22620411234716Z"); time_str = "2262-04-11 23:47:17"; - tp = ccf::ds::time_point_from_string(time_str); - conv = ccf::ds::to_x509_time_string(tp); + REQUIRE_THROWS_WITH( + ccf::ds::time_point_from_string(time_str), + "'2262-04-11 23:47:17' is too far in the future to be represented as a " + "system_clock::time_point"); + s = ccf::ds::since_epoch_from_string(time_str); + conv = ccf::ds::to_x509_time_string(s); CHECK(conv == "22620411234717Z"); } +TEST_CASE("Invalid times") +{ + REQUIRE_THROWS_WITH( + ccf::ds::time_point_from_string("hello"), + "'hello' does not match any accepted time format"); + + REQUIRE_THROWS_WITH( + ccf::ds::time_point_from_string("Monday"), + "'Monday' does not match any accepted time format"); + + REQUIRE_THROWS_WITH( + ccf::ds::time_point_from_string("April 1st, 1984"), + "'April 1st, 1984' does not match any accepted time format"); + + REQUIRE_THROWS_WITH( + ccf::ds::time_point_from_string("1111-1111"), + "'1111-1111' does not match any accepted time format"); +} + TEST_CASE("Create sign and verify certificates") { bool corrupt_csr = false; From b6b08a717dd37d4de9f0e7cce3f6cb54e2a5b2d5 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 6 Feb 2026 15:26:38 +0000 Subject: [PATCH 03/17] Separate seconds-since-epoch calculation from conversion to system_clock --- include/ccf/ds/x509_time_fmt.h | 61 ++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/include/ccf/ds/x509_time_fmt.h b/include/ccf/ds/x509_time_fmt.h index 1ea8d952b87..cc5a65a9e99 100644 --- a/include/ccf/ds/x509_time_fmt.h +++ b/include/ccf/ds/x509_time_fmt.h @@ -7,10 +7,10 @@ #include #include #include +#include #include #include #include - namespace ccf::ds { static inline std::string to_x509_time_string(const std::tm& time) @@ -26,7 +26,13 @@ namespace ccf::ds return to_x509_time_string(fmt::gmtime(time)); } - static inline std::chrono::system_clock::time_point time_point_from_string( + static inline std::string to_x509_time_string( + const std::chrono::seconds& seconds_since_epoch) + { + return to_x509_time_string(fmt::gmtime(seconds_since_epoch.count())); + } + + static inline std::chrono::seconds since_epoch_from_string( const std::string& time) { // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) @@ -44,9 +50,9 @@ namespace ccf::ds auto* sres = strptime(ts, afmt, &t); if (sres != nullptr && *sres == '\0') { - auto r = std::chrono::system_clock::from_time_t(timegm(&t)); - r -= std::chrono::seconds(t.tm_gmtoff); - return r; + auto r = timegm(&t); + r -= t.tm_gmtoff; + return std::chrono::seconds(r); } } @@ -94,11 +100,15 @@ namespace ccf::ds continue; } - system_clock::time_point r = (sys_days)date; - if (rs >= 6) - { - r += hours(h) + minutes(mn) + microseconds((long)(s * 1e6)); - } + struct tm tm = {}; + tm.tm_year = y - 1900; + tm.tm_mon = m - 1; + tm.tm_mday = d; + tm.tm_hour = h; + tm.tm_min = mn; + tm.tm_sec = (int)s; + + auto r = std::chrono::seconds(timegm(&tm)); if (rs >= 8) { r -= hours(oh) + minutes(om); @@ -113,6 +123,37 @@ namespace ccf::ds fmt::format("'{}' does not match any accepted time format", time)); } + static inline std::chrono::system_clock::time_point time_point_from_string( + const std::string& time) + { + const auto s = since_epoch_from_string(time); + + static constexpr auto range_max = + std::chrono::duration_cast( + std::chrono::system_clock::time_point::max().time_since_epoch()); + static constexpr auto range_min = + std::chrono::duration_cast( + std::chrono::system_clock::time_point::min().time_since_epoch()); + + if (s > range_max) + { + throw std::runtime_error(fmt::format( + "'{}' is too far in the future to be represented as a " + "system_clock::time_point", + time)); + } + + if (s < range_min) + { + throw std::runtime_error(fmt::format( + "'{}' is too far in the past to be represented as a " + "system_clock::time_point", + time)); + } + + return std::chrono::system_clock::time_point(s); + } + static inline std::string to_x509_time_string(const std::string& time) { return to_x509_time_string(time_point_from_string(time)); From f5aae5fb6d939f8efceae58fe16ed710329a22d6 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 6 Feb 2026 15:46:23 +0000 Subject: [PATCH 04/17] Prefer since_epoch_from_string to time_point_from_string --- src/crypto/certs.h | 2 +- src/crypto/openssl/verifier.cpp | 18 ++++++++++-------- src/endpoints/authentication/cert_auth.cpp | 10 ++-------- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/crypto/certs.h b/src/crypto/certs.h index 7151ba97c41..01354f1646c 100644 --- a/src/crypto/certs.h +++ b/src/crypto/certs.h @@ -17,7 +17,7 @@ namespace ccf::crypto using namespace std::chrono_literals; // Note: As per RFC 5280, the validity period runs until "notAfter" // _inclusive_ so substract one second from the validity period. - auto valid_to = ccf::ds::time_point_from_string(valid_from) + + auto valid_to = ccf::ds::since_epoch_from_string(valid_from) + std::chrono::days(validity_period_days) - 1s; return ccf::ds::to_x509_time_string(valid_to); } diff --git a/src/crypto/openssl/verifier.cpp b/src/crypto/openssl/verifier.cpp index 2b7793c2778..0cda9f4eb25 100644 --- a/src/crypto/openssl/verifier.cpp +++ b/src/crypto/openssl/verifier.cpp @@ -210,8 +210,9 @@ namespace ccf::crypto const std::chrono::system_clock::time_point& now) const { auto [from, to] = validity_period(); - auto tp_to = ccf::ds::time_point_from_string(to); - return std::chrono::duration_cast(tp_to - now) + auto s_to = ccf::ds::since_epoch_from_string(to); + return std::chrono::duration_cast( + s_to - now.time_since_epoch()) .count() + 1; } @@ -220,14 +221,15 @@ namespace ccf::crypto const std::chrono::system_clock::time_point& now) const { auto [from, to] = validity_period(); - auto tp_from = ccf::ds::time_point_from_string(from); - auto tp_to = ccf::ds::time_point_from_string(to); + auto s_from = ccf::ds::since_epoch_from_string(from); + auto s_to = ccf::ds::since_epoch_from_string(to); auto total_sec = - std::chrono::duration_cast(tp_to - tp_from) - .count() + + std::chrono::duration_cast(s_to - s_from).count() + + 1; + auto rem_sec = std::chrono::duration_cast( + s_to - now.time_since_epoch()) + .count() + 1; - auto rem_sec = - std::chrono::duration_cast(tp_to - now).count() + 1; return rem_sec / (double)total_sec; } } diff --git a/src/endpoints/authentication/cert_auth.cpp b/src/endpoints/authentication/cert_auth.cpp index 0acb43bb066..7f2d108d570 100644 --- a/src/endpoints/authentication/cert_auth.cpp +++ b/src/endpoints/authentication/cert_auth.cpp @@ -48,15 +48,9 @@ namespace ccf using namespace std::chrono; const auto valid_from_unix_time = - duration_cast( - ccf::ds::time_point_from_string(valid_from_timestring) - .time_since_epoch()) - .count(); + ccf::ds::since_epoch_from_string(valid_from_timestring).count(); const auto valid_to_unix_time = - duration_cast( - ccf::ds::time_point_from_string(valid_to_timestring) - .time_since_epoch()) - .count(); + ccf::ds::since_epoch_from_string(valid_to_timestring).count(); it = periods.insert( der, ValidityPeriod{valid_from_unix_time, valid_to_unix_time}); From 66f674b5855c6a9bf68a6a6777ad7ad2cb3827e8 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Fri, 6 Feb 2026 16:09:24 +0000 Subject: [PATCH 05/17] Add e2e tests of far-future certs --- .../custom_authorization.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/js-custom-authorization/custom_authorization.py b/tests/js-custom-authorization/custom_authorization.py index 89d8b8ef5af..3bc1504f028 100644 --- a/tests/js-custom-authorization/custom_authorization.py +++ b/tests/js-custom-authorization/custom_authorization.py @@ -337,6 +337,30 @@ def create_keypair(local_id, valid_from, validity_days): assert r.status_code == HTTPStatus.UNAUTHORIZED, r assert "Not After" in parse_error_message(r), r + LOG.info("Long-lived cert doesn't wraparound") + local_user_id = "long_lived" + valid_from = datetime.datetime.utcnow() + create_keypair(local_user_id, valid_from, 1_000_000) + network.consortium.add_user(primary, local_user_id) + + with primary.client(local_user_id) as c: + r = c.get("/app/cert") + assert r.status_code == HTTPStatus.OK, r + + LOG.info("Future Not-Before doesn't wraparound") + local_user_id = "distant_future" + # system_clock max representable time is currently 2262-04-11, so use a date after that to check for wraparound + valid_from = datetime.datetime(year=2262, month=4, day=12) + create_keypair(local_user_id, valid_from, 4) + network.consortium.add_user(primary, local_user_id) + + with primary.client(local_user_id) as c: + r = c.get("/app/cert") + assert r.status_code == HTTPStatus.UNAUTHORIZED, r + expected = f"certificate's Not Before validity period {int(valid_from.timestamp())}" + actual = parse_error_message(r) + assert expected in actual, r + return network From 1c88432e40a215719be882d2227bdbb0c07df2d2 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Mon, 9 Feb 2026 09:47:13 +0000 Subject: [PATCH 06/17] Format --- src/crypto/test/crypto.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index c51c270d8cc..138481a61ef 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -825,7 +825,7 @@ TEST_CASE("Invalid times") REQUIRE_THROWS_WITH( ccf::ds::time_point_from_string("April 1st, 1984"), "'April 1st, 1984' does not match any accepted time format"); - + REQUIRE_THROWS_WITH( ccf::ds::time_point_from_string("1111-1111"), "'1111-1111' does not match any accepted time format"); From 4bb5378c07d0e6ea348de27ed37f13fc468d5163 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Mon, 9 Feb 2026 11:45:45 +0000 Subject: [PATCH 07/17] Try a new Clock type, to minimise other diffs --- include/ccf/crypto/openssl/openssl_wrappers.h | 2 +- include/ccf/crypto/verifier.h | 5 +- include/ccf/ds/epoch_clock.h | 33 +++++++++ include/ccf/ds/x509_time_fmt.h | 70 +++++------------- src/crypto/certs.h | 2 +- src/crypto/openssl/verifier.cpp | 22 +++--- src/crypto/openssl/verifier.h | 4 +- src/crypto/test/crypto.cpp | 74 +++++++++++-------- src/endpoints/authentication/cert_auth.cpp | 10 ++- 9 files changed, 120 insertions(+), 102 deletions(-) create mode 100644 include/ccf/ds/epoch_clock.h diff --git a/include/ccf/crypto/openssl/openssl_wrappers.h b/include/ccf/crypto/openssl/openssl_wrappers.h index 70236ba688b..8a6c28f575f 100644 --- a/include/ccf/crypto/openssl/openssl_wrappers.h +++ b/include/ccf/crypto/openssl/openssl_wrappers.h @@ -359,7 +359,7 @@ namespace ccf::crypto::OpenSSL Unique_X509_TIME(ASN1_TIME* t) : Unique_SSL_OBJECT(t, ASN1_TIME_free, /*check_null=*/false) {} - Unique_X509_TIME(const std::chrono::system_clock::time_point& t) : + Unique_X509_TIME(const ccf::ds::EpochClock::time_point& t) : Unique_X509_TIME(ccf::ds::to_x509_time_string(t)) {} }; diff --git a/include/ccf/crypto/verifier.h b/include/ccf/crypto/verifier.h index f8b395d6602..30987ff6def 100644 --- a/include/ccf/crypto/verifier.h +++ b/include/ccf/crypto/verifier.h @@ -6,6 +6,7 @@ #include "ccf/crypto/jwk.h" #include "ccf/crypto/pem.h" #include "ccf/crypto/rsa_public_key.h" +#include "ccf/ds/epoch_clock.h" #include @@ -154,11 +155,11 @@ namespace ccf::crypto /** The number of seconds of the validity period of the * certificate remaining */ [[nodiscard]] virtual size_t remaining_seconds( - const std::chrono::system_clock::time_point& now) const = 0; + const ccf::ds::EpochClock::time_point& now) const = 0; /** The percentage of the validity period of the certificate remaining */ [[nodiscard]] virtual double remaining_percentage( - const std::chrono::system_clock::time_point& now) const = 0; + const ccf::ds::EpochClock::time_point& now) const = 0; /** The subject name of the certificate */ [[nodiscard]] virtual std::string subject() const = 0; diff --git a/include/ccf/ds/epoch_clock.h b/include/ccf/ds/epoch_clock.h new file mode 100644 index 00000000000..fb582cabf4b --- /dev/null +++ b/include/ccf/ds/epoch_clock.h @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the Apache 2.0 License. +#pragma once + +#include + +namespace ccf::ds +{ + // TODO: Docs + struct EpochClock + { + using duration = std::chrono::seconds; + using rep = duration::rep; + using period = duration::period; + using time_point = std::chrono::time_point; + static constexpr bool is_steady = false; + + static time_point now() noexcept + { + return time_point(duration(std::time(nullptr))); + } + + static std::time_t to_time_t(const time_point& t) noexcept + { + return std::time_t(t.time_since_epoch().count()); + } + + static time_point from_time_t(std::time_t t) noexcept + { + return time_point(duration(t)); + } + }; +} diff --git a/include/ccf/ds/x509_time_fmt.h b/include/ccf/ds/x509_time_fmt.h index cc5a65a9e99..53af3ed3907 100644 --- a/include/ccf/ds/x509_time_fmt.h +++ b/include/ccf/ds/x509_time_fmt.h @@ -3,14 +3,16 @@ #pragma once #define FMT_HEADER_ONLY +#include "ccf/ds/epoch_clock.h" + #include #include #include #include -#include #include #include #include + namespace ccf::ds { static inline std::string to_x509_time_string(const std::tm& time) @@ -21,18 +23,18 @@ namespace ccf::ds } static inline std::string to_x509_time_string( - const std::chrono::system_clock::time_point& time) + const ccf::ds::EpochClock::time_point& time) { - return to_x509_time_string(fmt::gmtime(time)); + return to_x509_time_string(fmt::gmtime(EpochClock::to_time_t(time))); } static inline std::string to_x509_time_string( - const std::chrono::seconds& seconds_since_epoch) + const std::chrono::system_clock::time_point& time) { - return to_x509_time_string(fmt::gmtime(seconds_since_epoch.count())); + return to_x509_time_string(fmt::gmtime(time)); } - static inline std::chrono::seconds since_epoch_from_string( + static inline ccf::ds::EpochClock::time_point time_point_from_string( const std::string& time) { // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) @@ -50,9 +52,9 @@ namespace ccf::ds auto* sres = strptime(ts, afmt, &t); if (sres != nullptr && *sres == '\0') { - auto r = timegm(&t); - r -= t.tm_gmtoff; - return std::chrono::seconds(r); + auto r = ccf::ds::EpochClock::from_time_t(timegm(&t)); + r -= std::chrono::seconds(t.tm_gmtoff); + return r; } } @@ -100,20 +102,17 @@ namespace ccf::ds continue; } - struct tm tm = {}; - tm.tm_year = y - 1900; - tm.tm_mon = m - 1; - tm.tm_mday = d; - tm.tm_hour = h; - tm.tm_min = mn; - tm.tm_sec = (int)s; - - auto r = std::chrono::seconds(timegm(&tm)); + std::chrono::system_clock::time_point tp = sys_days(date); + if (rs >= 6) + { + tp += hours(h) + minutes(mn) + microseconds((long)(s * 1e6)); + } if (rs >= 8) { - r -= hours(oh) + minutes(om); + tp -= hours(oh) + minutes(om); } - return r; + + return ccf::ds::EpochClock::from_time_t(system_clock::to_time_t(tp)); } } } @@ -123,37 +122,6 @@ namespace ccf::ds fmt::format("'{}' does not match any accepted time format", time)); } - static inline std::chrono::system_clock::time_point time_point_from_string( - const std::string& time) - { - const auto s = since_epoch_from_string(time); - - static constexpr auto range_max = - std::chrono::duration_cast( - std::chrono::system_clock::time_point::max().time_since_epoch()); - static constexpr auto range_min = - std::chrono::duration_cast( - std::chrono::system_clock::time_point::min().time_since_epoch()); - - if (s > range_max) - { - throw std::runtime_error(fmt::format( - "'{}' is too far in the future to be represented as a " - "system_clock::time_point", - time)); - } - - if (s < range_min) - { - throw std::runtime_error(fmt::format( - "'{}' is too far in the past to be represented as a " - "system_clock::time_point", - time)); - } - - return std::chrono::system_clock::time_point(s); - } - static inline std::string to_x509_time_string(const std::string& time) { return to_x509_time_string(time_point_from_string(time)); diff --git a/src/crypto/certs.h b/src/crypto/certs.h index 01354f1646c..7151ba97c41 100644 --- a/src/crypto/certs.h +++ b/src/crypto/certs.h @@ -17,7 +17,7 @@ namespace ccf::crypto using namespace std::chrono_literals; // Note: As per RFC 5280, the validity period runs until "notAfter" // _inclusive_ so substract one second from the validity period. - auto valid_to = ccf::ds::since_epoch_from_string(valid_from) + + auto valid_to = ccf::ds::time_point_from_string(valid_from) + std::chrono::days(validity_period_days) - 1s; return ccf::ds::to_x509_time_string(valid_to); } diff --git a/src/crypto/openssl/verifier.cpp b/src/crypto/openssl/verifier.cpp index 0cda9f4eb25..35d5e847e3a 100644 --- a/src/crypto/openssl/verifier.cpp +++ b/src/crypto/openssl/verifier.cpp @@ -207,29 +207,27 @@ namespace ccf::crypto } size_t Verifier_OpenSSL::remaining_seconds( - const std::chrono::system_clock::time_point& now) const + const ccf::ds::EpochClock::time_point& now) const { auto [from, to] = validity_period(); - auto s_to = ccf::ds::since_epoch_from_string(to); - return std::chrono::duration_cast( - s_to - now.time_since_epoch()) + auto tp_to = ccf::ds::time_point_from_string(to); + return std::chrono::duration_cast(tp_to - now) .count() + 1; } double Verifier_OpenSSL::remaining_percentage( - const std::chrono::system_clock::time_point& now) const + const ccf::ds::EpochClock::time_point& now) const { auto [from, to] = validity_period(); - auto s_from = ccf::ds::since_epoch_from_string(from); - auto s_to = ccf::ds::since_epoch_from_string(to); + auto tp_from = ccf::ds::time_point_from_string(from); + auto tp_to = ccf::ds::time_point_from_string(to); auto total_sec = - std::chrono::duration_cast(s_to - s_from).count() + - 1; - auto rem_sec = std::chrono::duration_cast( - s_to - now.time_since_epoch()) - .count() + + std::chrono::duration_cast(tp_to - tp_from) + .count() + 1; + auto rem_sec = + std::chrono::duration_cast(tp_to - now).count() + 1; return rem_sec / (double)total_sec; } } diff --git a/src/crypto/openssl/verifier.h b/src/crypto/openssl/verifier.h index 7302764d908..4636e98df47 100644 --- a/src/crypto/openssl/verifier.h +++ b/src/crypto/openssl/verifier.h @@ -38,10 +38,10 @@ namespace ccf::crypto std::pair validity_period() const override; size_t remaining_seconds( - const std::chrono::system_clock::time_point& now) const override; + const ccf::ds::EpochClock::time_point& now) const override; double remaining_percentage( - const std::chrono::system_clock::time_point& now) const override; + const ccf::ds::EpochClock::time_point& now) const override; std::string subject() const override; }; diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index 138481a61ef..69c3b939dc9 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -191,7 +191,7 @@ ccf::crypto::Pem generate_self_signed_cert( constexpr size_t certificate_validity_period_days = 365; using namespace std::literals; auto valid_from = - ccf::ds::to_x509_time_string(std::chrono::system_clock::now() - 24h); + ccf::ds::to_x509_time_string(ccf::ds::EpochClock::now() - 24h); return ccf::crypto::create_self_signed_cert( kp, name, {}, valid_from, certificate_validity_period_days); @@ -783,33 +783,45 @@ TEST_CASE("Non-ASN.1 timepoint formats") TEST_CASE("Timepoint bounds") { - auto time_str = "1677-09-21 00:12:44"; - auto tp = ccf::ds::time_point_from_string(time_str); - auto conv = ccf::ds::to_x509_time_string(tp); - REQUIRE(conv == "16770921001244Z"); + // Can handle values beyond bounds of system_clock::time_point + { + INFO("Beyond system_clock::time_point min"); + auto time_str = "1677-09-21 00:12:44"; + auto tp = ccf::ds::time_point_from_string(time_str); + auto conv = ccf::ds::to_x509_time_string(tp); + REQUIRE(conv == "16770921001244Z"); + + time_str = "1677-09-21 00:12:43"; + tp = ccf::ds::time_point_from_string(time_str); + conv = ccf::ds::to_x509_time_string(tp); + REQUIRE(conv == "16770921001243Z"); + } - time_str = "1677-09-21 00:12:43"; - REQUIRE_THROWS_WITH( - ccf::ds::time_point_from_string(time_str), - "'1677-09-21 00:12:43' is too far in the past to be represented as a " - "system_clock::time_point"); - auto s = ccf::ds::since_epoch_from_string(time_str); - conv = ccf::ds::to_x509_time_string(s); - CHECK(conv == "16770921001243Z"); - - time_str = "2262-04-11 23:47:16"; - tp = ccf::ds::time_point_from_string(time_str); - conv = ccf::ds::to_x509_time_string(tp); - REQUIRE(conv == "22620411234716Z"); + { + INFO("Beyond system_clock::time_point max"); + auto time_str = "2262-04-11 23:47:16"; + auto tp = ccf::ds::time_point_from_string(time_str); + auto conv = ccf::ds::to_x509_time_string(tp); + REQUIRE(conv == "22620411234716Z"); + + time_str = "2262-04-11 23:47:17"; + tp = ccf::ds::time_point_from_string(time_str); + conv = ccf::ds::to_x509_time_string(tp); + CHECK(conv == "22620411234717Z"); + } - time_str = "2262-04-11 23:47:17"; - REQUIRE_THROWS_WITH( - ccf::ds::time_point_from_string(time_str), - "'2262-04-11 23:47:17' is too far in the future to be represented as a " - "system_clock::time_point"); - s = ccf::ds::since_epoch_from_string(time_str); - conv = ccf::ds::to_x509_time_string(s); - CHECK(conv == "22620411234717Z"); + { + INFO("Approx ASN.1 format bounds"); + auto time_str = "9999-12-31 23:59:59"; + auto tp = ccf::ds::time_point_from_string(time_str); + auto conv = ccf::ds::to_x509_time_string(tp); + REQUIRE(conv == "99991231235959Z"); + + time_str = "0000-01-01 00:00:00"; + tp = ccf::ds::time_point_from_string(time_str); + conv = ccf::ds::to_x509_time_string(tp); + CHECK(conv == "00000101000000Z"); + } } TEST_CASE("Invalid times") @@ -935,7 +947,7 @@ TEST_CASE("AES-GCM convenience functions") TEST_CASE("x509 time") { - auto time = std::chrono::system_clock::now(); + auto time = ccf::ds::EpochClock::now(); auto next_minute_time = time + 1min; auto next_day_time = time + 24h; @@ -947,8 +959,8 @@ TEST_CASE("x509 time") { struct Input { - std::chrono::system_clock::time_point from; - std::chrono::system_clock::time_point to; + ccf::ds::EpochClock::time_point from; + ccf::ds::EpochClock::time_point to; std::optional maximum_validity_period_days = std::nullopt; }; Input input; @@ -983,7 +995,7 @@ TEST_CASE("x509 time") INFO("Adjust time"); { - std::vector times = { + std::vector times = { time, next_day_time, next_day_time}; size_t days_offset = 100; @@ -1465,7 +1477,7 @@ TEST_CASE("Do not trust non-ca certs") constexpr size_t certificate_validity_period_days = 365; using namespace std::literals; auto valid_from = - ccf::ds::to_x509_time_string(std::chrono::system_clock::now() - 24h); + ccf::ds::to_x509_time_string(ccf::ds::EpochClock::now() - 24h); auto valid_to = compute_cert_valid_to_string( valid_from, certificate_validity_period_days); std::vector subject_alt_names = {}; diff --git a/src/endpoints/authentication/cert_auth.cpp b/src/endpoints/authentication/cert_auth.cpp index 7f2d108d570..0acb43bb066 100644 --- a/src/endpoints/authentication/cert_auth.cpp +++ b/src/endpoints/authentication/cert_auth.cpp @@ -48,9 +48,15 @@ namespace ccf using namespace std::chrono; const auto valid_from_unix_time = - ccf::ds::since_epoch_from_string(valid_from_timestring).count(); + duration_cast( + ccf::ds::time_point_from_string(valid_from_timestring) + .time_since_epoch()) + .count(); const auto valid_to_unix_time = - ccf::ds::since_epoch_from_string(valid_to_timestring).count(); + duration_cast( + ccf::ds::time_point_from_string(valid_to_timestring) + .time_since_epoch()) + .count(); it = periods.insert( der, ValidityPeriod{valid_from_unix_time, valid_to_unix_time}); From 866560740d51e4f2fb9c195321f0368e0e98e9b5 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Mon, 9 Feb 2026 11:48:28 +0000 Subject: [PATCH 08/17] Docs and format --- include/ccf/ds/epoch_clock.h | 5 ++++- tests/js-custom-authorization/custom_authorization.py | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/include/ccf/ds/epoch_clock.h b/include/ccf/ds/epoch_clock.h index fb582cabf4b..2af2f1081c0 100644 --- a/include/ccf/ds/epoch_clock.h +++ b/include/ccf/ds/epoch_clock.h @@ -6,7 +6,10 @@ namespace ccf::ds { - // TODO: Docs + // A custom clock type for handling certificate validity periods, which are + // defined in terms of seconds since the epoch. This avoids issues with + // system_clock::time_point being unable to represent times after 2262-04-11 + // 23:47:17 UTC (due to tracking nanosecond precision). struct EpochClock { using duration = std::chrono::seconds; diff --git a/tests/js-custom-authorization/custom_authorization.py b/tests/js-custom-authorization/custom_authorization.py index 3bc1504f028..313ce636b5b 100644 --- a/tests/js-custom-authorization/custom_authorization.py +++ b/tests/js-custom-authorization/custom_authorization.py @@ -357,7 +357,9 @@ def create_keypair(local_id, valid_from, validity_days): with primary.client(local_user_id) as c: r = c.get("/app/cert") assert r.status_code == HTTPStatus.UNAUTHORIZED, r - expected = f"certificate's Not Before validity period {int(valid_from.timestamp())}" + expected = ( + f"certificate's Not Before validity period {int(valid_from.timestamp())}" + ) actual = parse_error_message(r) assert expected in actual, r From 6a0c74010232997e8451de49e5b865a7cc3f2d52 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Mon, 9 Feb 2026 13:50:22 +0000 Subject: [PATCH 09/17] Rename --- include/ccf/crypto/openssl/openssl_wrappers.h | 2 +- include/ccf/crypto/verifier.h | 6 ++-- include/ccf/ds/epoch_clock.h | 36 ------------------- include/ccf/ds/nonstd.h | 30 ++++++++++++++++ include/ccf/ds/x509_time_fmt.h | 16 +++++---- src/crypto/openssl/verifier.cpp | 4 +-- src/crypto/openssl/verifier.h | 4 +-- src/crypto/test/crypto.cpp | 12 +++---- 8 files changed, 53 insertions(+), 57 deletions(-) delete mode 100644 include/ccf/ds/epoch_clock.h diff --git a/include/ccf/crypto/openssl/openssl_wrappers.h b/include/ccf/crypto/openssl/openssl_wrappers.h index 8a6c28f575f..bc4a1ebc955 100644 --- a/include/ccf/crypto/openssl/openssl_wrappers.h +++ b/include/ccf/crypto/openssl/openssl_wrappers.h @@ -359,7 +359,7 @@ namespace ccf::crypto::OpenSSL Unique_X509_TIME(ASN1_TIME* t) : Unique_SSL_OBJECT(t, ASN1_TIME_free, /*check_null=*/false) {} - Unique_X509_TIME(const ccf::ds::EpochClock::time_point& t) : + Unique_X509_TIME(const ccf::nonstd::SystemClock::time_point& t) : Unique_X509_TIME(ccf::ds::to_x509_time_string(t)) {} }; diff --git a/include/ccf/crypto/verifier.h b/include/ccf/crypto/verifier.h index 30987ff6def..3b44898fa99 100644 --- a/include/ccf/crypto/verifier.h +++ b/include/ccf/crypto/verifier.h @@ -6,7 +6,7 @@ #include "ccf/crypto/jwk.h" #include "ccf/crypto/pem.h" #include "ccf/crypto/rsa_public_key.h" -#include "ccf/ds/epoch_clock.h" +#include "ccf/ds/nonstd.h" #include @@ -155,11 +155,11 @@ namespace ccf::crypto /** The number of seconds of the validity period of the * certificate remaining */ [[nodiscard]] virtual size_t remaining_seconds( - const ccf::ds::EpochClock::time_point& now) const = 0; + const ccf::nonstd::SystemClock::time_point& now) const = 0; /** The percentage of the validity period of the certificate remaining */ [[nodiscard]] virtual double remaining_percentage( - const ccf::ds::EpochClock::time_point& now) const = 0; + const ccf::nonstd::SystemClock::time_point& now) const = 0; /** The subject name of the certificate */ [[nodiscard]] virtual std::string subject() const = 0; diff --git a/include/ccf/ds/epoch_clock.h b/include/ccf/ds/epoch_clock.h deleted file mode 100644 index 2af2f1081c0..00000000000 --- a/include/ccf/ds/epoch_clock.h +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the Apache 2.0 License. -#pragma once - -#include - -namespace ccf::ds -{ - // A custom clock type for handling certificate validity periods, which are - // defined in terms of seconds since the epoch. This avoids issues with - // system_clock::time_point being unable to represent times after 2262-04-11 - // 23:47:17 UTC (due to tracking nanosecond precision). - struct EpochClock - { - using duration = std::chrono::seconds; - using rep = duration::rep; - using period = duration::period; - using time_point = std::chrono::time_point; - static constexpr bool is_steady = false; - - static time_point now() noexcept - { - return time_point(duration(std::time(nullptr))); - } - - static std::time_t to_time_t(const time_point& t) noexcept - { - return std::time_t(t.time_since_epoch().count()); - } - - static time_point from_time_t(std::time_t t) noexcept - { - return time_point(duration(t)); - } - }; -} diff --git a/include/ccf/ds/nonstd.h b/include/ccf/ds/nonstd.h index fca155d0ebc..0ac03f6be81 100644 --- a/include/ccf/ds/nonstd.h +++ b/include/ccf/ds/nonstd.h @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -210,9 +211,38 @@ namespace ccf::nonstd *fd = -1; } } + using CloseFdGuard = std::unique_ptr; static inline CloseFdGuard make_close_fd_guard(int* fd) { return {fd, close_fd}; } + + // A custom clock type for handling certificate validity periods, which are + // defined in terms of seconds since the epoch. This avoids issues with + // system_clock::time_point being unable to represent times after 2262-04-11 + // 23:47:17 UTC (due to tracking nanosecond precision). + struct SystemClock + { + using duration = std::chrono::seconds; + using rep = duration::rep; + using period = duration::period; + using time_point = std::chrono::time_point; + static constexpr bool is_steady = false; + + static time_point now() noexcept + { + return time_point(duration(std::time(nullptr))); + } + + static std::time_t to_time_t(const time_point& t) noexcept + { + return std::time_t(t.time_since_epoch().count()); + } + + static time_point from_time_t(std::time_t t) noexcept + { + return time_point(duration(t)); + } + }; } \ No newline at end of file diff --git a/include/ccf/ds/x509_time_fmt.h b/include/ccf/ds/x509_time_fmt.h index 53af3ed3907..97bde350060 100644 --- a/include/ccf/ds/x509_time_fmt.h +++ b/include/ccf/ds/x509_time_fmt.h @@ -2,10 +2,10 @@ // Licensed under the Apache 2.0 License. #pragma once -#define FMT_HEADER_ONLY -#include "ccf/ds/epoch_clock.h" +#include "ccf/ds/nonstd.h" #include +#define FMT_HEADER_ONLY #include #include #include @@ -23,9 +23,10 @@ namespace ccf::ds } static inline std::string to_x509_time_string( - const ccf::ds::EpochClock::time_point& time) + const ccf::nonstd::SystemClock::time_point& time) { - return to_x509_time_string(fmt::gmtime(EpochClock::to_time_t(time))); + return to_x509_time_string( + fmt::gmtime(ccf::nonstd::SystemClock::to_time_t(time))); } static inline std::string to_x509_time_string( @@ -34,7 +35,7 @@ namespace ccf::ds return to_x509_time_string(fmt::gmtime(time)); } - static inline ccf::ds::EpochClock::time_point time_point_from_string( + static inline ccf::nonstd::SystemClock::time_point time_point_from_string( const std::string& time) { // NOLINTBEGIN(cppcoreguidelines-avoid-magic-numbers,readability-magic-numbers) @@ -52,7 +53,7 @@ namespace ccf::ds auto* sres = strptime(ts, afmt, &t); if (sres != nullptr && *sres == '\0') { - auto r = ccf::ds::EpochClock::from_time_t(timegm(&t)); + auto r = ccf::nonstd::SystemClock::from_time_t(timegm(&t)); r -= std::chrono::seconds(t.tm_gmtoff); return r; } @@ -112,7 +113,8 @@ namespace ccf::ds tp -= hours(oh) + minutes(om); } - return ccf::ds::EpochClock::from_time_t(system_clock::to_time_t(tp)); + return ccf::nonstd::SystemClock::from_time_t( + system_clock::to_time_t(tp)); } } } diff --git a/src/crypto/openssl/verifier.cpp b/src/crypto/openssl/verifier.cpp index 35d5e847e3a..d828f88c7a9 100644 --- a/src/crypto/openssl/verifier.cpp +++ b/src/crypto/openssl/verifier.cpp @@ -207,7 +207,7 @@ namespace ccf::crypto } size_t Verifier_OpenSSL::remaining_seconds( - const ccf::ds::EpochClock::time_point& now) const + const ccf::nonstd::SystemClock::time_point& now) const { auto [from, to] = validity_period(); auto tp_to = ccf::ds::time_point_from_string(to); @@ -217,7 +217,7 @@ namespace ccf::crypto } double Verifier_OpenSSL::remaining_percentage( - const ccf::ds::EpochClock::time_point& now) const + const ccf::nonstd::SystemClock::time_point& now) const { auto [from, to] = validity_period(); auto tp_from = ccf::ds::time_point_from_string(from); diff --git a/src/crypto/openssl/verifier.h b/src/crypto/openssl/verifier.h index 4636e98df47..b876453c40e 100644 --- a/src/crypto/openssl/verifier.h +++ b/src/crypto/openssl/verifier.h @@ -38,10 +38,10 @@ namespace ccf::crypto std::pair validity_period() const override; size_t remaining_seconds( - const ccf::ds::EpochClock::time_point& now) const override; + const ccf::nonstd::SystemClock::time_point& now) const override; double remaining_percentage( - const ccf::ds::EpochClock::time_point& now) const override; + const ccf::nonstd::SystemClock::time_point& now) const override; std::string subject() const override; }; diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index 69c3b939dc9..3ada12a0039 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -191,7 +191,7 @@ ccf::crypto::Pem generate_self_signed_cert( constexpr size_t certificate_validity_period_days = 365; using namespace std::literals; auto valid_from = - ccf::ds::to_x509_time_string(ccf::ds::EpochClock::now() - 24h); + ccf::ds::to_x509_time_string(ccf::nonstd::SystemClock::now() - 24h); return ccf::crypto::create_self_signed_cert( kp, name, {}, valid_from, certificate_validity_period_days); @@ -947,7 +947,7 @@ TEST_CASE("AES-GCM convenience functions") TEST_CASE("x509 time") { - auto time = ccf::ds::EpochClock::now(); + auto time = ccf::nonstd::SystemClock::now(); auto next_minute_time = time + 1min; auto next_day_time = time + 24h; @@ -959,8 +959,8 @@ TEST_CASE("x509 time") { struct Input { - ccf::ds::EpochClock::time_point from; - ccf::ds::EpochClock::time_point to; + ccf::nonstd::SystemClock::time_point from; + ccf::nonstd::SystemClock::time_point to; std::optional maximum_validity_period_days = std::nullopt; }; Input input; @@ -995,7 +995,7 @@ TEST_CASE("x509 time") INFO("Adjust time"); { - std::vector times = { + std::vector times = { time, next_day_time, next_day_time}; size_t days_offset = 100; @@ -1477,7 +1477,7 @@ TEST_CASE("Do not trust non-ca certs") constexpr size_t certificate_validity_period_days = 365; using namespace std::literals; auto valid_from = - ccf::ds::to_x509_time_string(ccf::ds::EpochClock::now() - 24h); + ccf::ds::to_x509_time_string(ccf::nonstd::SystemClock::now() - 24h); auto valid_to = compute_cert_valid_to_string( valid_from, certificate_validity_period_days); std::vector subject_alt_names = {}; From cc7dd1b22d10629a134181d26a0f886e6ff62430 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Mon, 9 Feb 2026 13:50:27 +0000 Subject: [PATCH 10/17] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa2a3242c25..8525bd82725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed - Only rollback uncommittable indices during become_leader (#7620) +- 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) ## [7.0.0-dev9] From f9acac146dc4106d1352804da5f7154b72b48581 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Mon, 9 Feb 2026 13:51:45 +0000 Subject: [PATCH 11/17] e2e test of representable range --- .../js-custom-authorization/custom_authorization.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/js-custom-authorization/custom_authorization.py b/tests/js-custom-authorization/custom_authorization.py index 313ce636b5b..7cd086590ec 100644 --- a/tests/js-custom-authorization/custom_authorization.py +++ b/tests/js-custom-authorization/custom_authorization.py @@ -363,6 +363,19 @@ def create_keypair(local_id, valid_from, validity_days): actual = parse_error_message(r) assert expected in actual, r + LOG.info("Representable range") + local_user_id = "representable" + # Python crypto enforces minimum Not-Before of 1950-01-01 + valid_from = datetime.datetime(year=1950, month=1, day=1) + # Probe maximum validity range + validity_days = (datetime.datetime(year=9999, month=12, day=31) - valid_from).days + create_keypair(local_user_id, valid_from, validity_days) + network.consortium.add_user(primary, local_user_id) + + with primary.client(local_user_id) as c: + r = c.get("/app/cert") + assert r.status_code == HTTPStatus.OK, r + return network From f78714503fac781a563969bacf4ef58f783e7470 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Mon, 9 Feb 2026 14:09:42 +0000 Subject: [PATCH 12/17] Robot --- include/ccf/ds/nonstd.h | 1 + .../custom_authorization.py | 24 ++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/include/ccf/ds/nonstd.h b/include/ccf/ds/nonstd.h index 0ac03f6be81..f77c72689d6 100644 --- a/include/ccf/ds/nonstd.h +++ b/include/ccf/ds/nonstd.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include diff --git a/tests/js-custom-authorization/custom_authorization.py b/tests/js-custom-authorization/custom_authorization.py index 7cd086590ec..c57999e57e0 100644 --- a/tests/js-custom-authorization/custom_authorization.py +++ b/tests/js-custom-authorization/custom_authorization.py @@ -305,7 +305,10 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("User with old cert cannot call user-authenticated endpoint") local_user_id = "in_the_past" create_keypair( - local_user_id, datetime.datetime.utcnow() - datetime.timedelta(days=50), 3 + local_user_id, + datetime.datetime.now(timezone=datetime.timezone.utc) + - datetime.timedelta(days=50), + 3, ) network.consortium.add_user(primary, local_user_id) @@ -317,7 +320,10 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("User with future cert cannot call user-authenticated endpoint") local_user_id = "in_the_future" create_keypair( - local_user_id, datetime.datetime.utcnow() + datetime.timedelta(days=50), 3 + local_user_id, + datetime.datetime.now(timezone=datetime.timezone.utc) + + datetime.timedelta(days=50), + 3, ) network.consortium.add_user(primary, local_user_id) @@ -328,7 +334,9 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("No leeway added to cert time evaluation") local_user_id = "just_expired" - valid_from = datetime.datetime.utcnow() - datetime.timedelta(days=1, seconds=2) + valid_from = datetime.datetime.now( + timezone=datetime.timezone.utc + ) - datetime.timedelta(days=1, seconds=2) create_keypair(local_user_id, valid_from, 1) network.consortium.add_user(primary, local_user_id) @@ -339,7 +347,7 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("Long-lived cert doesn't wraparound") local_user_id = "long_lived" - valid_from = datetime.datetime.utcnow() + valid_from = datetime.datetime.now(timezone=datetime.timezone.utc) create_keypair(local_user_id, valid_from, 1_000_000) network.consortium.add_user(primary, local_user_id) @@ -350,7 +358,9 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("Future Not-Before doesn't wraparound") local_user_id = "distant_future" # system_clock max representable time is currently 2262-04-11, so use a date after that to check for wraparound - valid_from = datetime.datetime(year=2262, month=4, day=12) + valid_from = datetime.datetime( + year=2262, month=4, day=12, timezone=datetime.timezone.utc + ) create_keypair(local_user_id, valid_from, 4) network.consortium.add_user(primary, local_user_id) @@ -366,7 +376,9 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("Representable range") local_user_id = "representable" # Python crypto enforces minimum Not-Before of 1950-01-01 - valid_from = datetime.datetime(year=1950, month=1, day=1) + valid_from = datetime.datetime( + year=1950, month=1, day=1, timezone=datetime.timezone.utc + ) # Probe maximum validity range validity_days = (datetime.datetime(year=9999, month=12, day=31) - valid_from).days create_keypair(local_user_id, valid_from, validity_days) From 3bd9db28bad61cc61fa8e8d4a93ca233eaa6d33a Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Mon, 9 Feb 2026 14:30:36 +0000 Subject: [PATCH 13/17] Good robot --- include/ccf/ds/x509_time_fmt.h | 4 +++- src/crypto/test/crypto.cpp | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/include/ccf/ds/x509_time_fmt.h b/include/ccf/ds/x509_time_fmt.h index 97bde350060..5ccbb543774 100644 --- a/include/ccf/ds/x509_time_fmt.h +++ b/include/ccf/ds/x509_time_fmt.h @@ -110,7 +110,9 @@ namespace ccf::ds } if (rs >= 8) { - tp -= hours(oh) + minutes(om); + auto offset = hours(oh) + + minutes(oh < 0 ? -static_cast(om) : static_cast(om)); + tp -= offset; } return ccf::nonstd::SystemClock::from_time_t( diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index 3ada12a0039..f7c49161d67 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -750,6 +750,11 @@ TEST_CASE("Non-ASN.1 timepoint formats") conv = ccf::ds::to_x509_time_string(tp); REQUIRE(conv == "20220405215327Z"); + time_str = "2026-02-09 05:00:00 -03:30"; + tp = ccf::ds::time_point_from_string(time_str); + conv = ccf::ds::to_x509_time_string(tp); + REQUIRE(conv == "20260209083000Z"); + time_str = "2022-04-07T10:37:49.567612"; tp = ccf::ds::time_point_from_string(time_str); conv = ccf::ds::to_x509_time_string(tp); From e9c9aae160c9efd95c19c6be41d966dd8ba0cc6e Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Mon, 9 Feb 2026 15:30:50 +0000 Subject: [PATCH 14/17] Robot suggestion: avoid conversion to sys_days --- include/ccf/ds/x509_time_fmt.h | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/include/ccf/ds/x509_time_fmt.h b/include/ccf/ds/x509_time_fmt.h index 5ccbb543774..2df75ef0e5c 100644 --- a/include/ccf/ds/x509_time_fmt.h +++ b/include/ccf/ds/x509_time_fmt.h @@ -103,20 +103,30 @@ namespace ccf::ds continue; } - std::chrono::system_clock::time_point tp = sys_days(date); + // Build a struct tm and use timegm() to convert to time_t + // directly, avoiding system_clock::time_point which can + // overflow for dates outside ~1677-2262. + struct tm t = {}; + t.tm_year = static_cast(y) - 1900; + t.tm_mon = static_cast(m) - 1; + t.tm_mday = static_cast(d); if (rs >= 6) { - tp += hours(h) + minutes(mn) + microseconds((long)(s * 1e6)); + t.tm_hour = static_cast(h); + t.tm_min = static_cast(mn); + t.tm_sec = static_cast(s); } + + auto tt = timegm(&t); + if (rs >= 8) { - auto offset = hours(oh) + - minutes(oh < 0 ? -static_cast(om) : static_cast(om)); - tp -= offset; + auto offset_secs = oh * 3600 + + (oh < 0 ? -static_cast(om) : static_cast(om)) * 60; + tt -= offset_secs; } - return ccf::nonstd::SystemClock::from_time_t( - system_clock::to_time_t(tp)); + return ccf::nonstd::SystemClock::from_time_t(tt); } } } From 2fa5d8c8bdca2057b1596c06b3d2ddd19de614d4 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Mon, 9 Feb 2026 15:31:15 +0000 Subject: [PATCH 15/17] Robot: Fix conversion from strange formats, add tests --- include/ccf/ds/x509_time_fmt.h | 5 ++--- src/crypto/test/crypto.cpp | 31 +++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/include/ccf/ds/x509_time_fmt.h b/include/ccf/ds/x509_time_fmt.h index 2df75ef0e5c..f8b4c58f332 100644 --- a/include/ccf/ds/x509_time_fmt.h +++ b/include/ccf/ds/x509_time_fmt.h @@ -65,8 +65,8 @@ namespace ccf::ds {"%04u-%02u-%02u %02u:%02u:%f %d:%02u", 8}, {"%04u-%02u-%02uT%02u:%02u:%f %d:%02u", 8}, {"%04u-%02u-%02u %02u:%02u:%f %03d %02u", 8}, - {"%02u%02u%02u%02u%02u%02f%03d%02u", 8}, - {"%04u%02u%02u%02u%02u%02f%03d%02u", 8}, + {"%02u%02u%02u%02u%02u%f%03d%02u", 8}, + {"%04u%02u%02u%02u%02u%f%03d%02u", 8}, {"%04u-%02u-%02uT%02u:%02u:%f", 6}, {"%04u-%02u-%02u %02u:%02u:%f", 6}}; @@ -85,7 +85,6 @@ namespace ccf::ds if (rs >= 1 && rs == n) { using namespace std::chrono; - if (strncmp(fmt, "%02u", 4) == 0) { // ASN.1 two-digit year range diff --git a/src/crypto/test/crypto.cpp b/src/crypto/test/crypto.cpp index f7c49161d67..1d46d60f2ae 100644 --- a/src/crypto/test/crypto.cpp +++ b/src/crypto/test/crypto.cpp @@ -822,10 +822,33 @@ TEST_CASE("Timepoint bounds") auto conv = ccf::ds::to_x509_time_string(tp); REQUIRE(conv == "99991231235959Z"); - time_str = "0000-01-01 00:00:00"; - tp = ccf::ds::time_point_from_string(time_str); - conv = ccf::ds::to_x509_time_string(tp); - CHECK(conv == "00000101000000Z"); + INFO("sscanf variants of near-min value"); + for (auto time_str : { + "0001-02-03 04:05:06", + "0001-02-03 04:05:06.700000 +0:00", + "0001-02-03 12:14:06.700000 +8:09", + "0001-02-02 19:56:06.700000 -8:09", + + "0001-02-03T04:05:06.700000 +0:00", + "0001-02-03T12:14:06.700000 +8:09", + "0001-02-02T19:56:06.700000 -8:09", + + "0001-02-03 04:05:06.700000 +00 00", + "0001-02-03 12:14:06.700000 +08:09", + "0001-02-02 19:56:06.700000 -08:09", + + "00010203040506.700000+0000", + "00010203121406.700000+0809", + "00010202195606.700000-0809", + + "0001-02-03T04:05:06.700000", + "0001-02-03 04:05:06.700000", + }) + { + tp = ccf::ds::time_point_from_string(time_str); + conv = ccf::ds::to_x509_time_string(tp); + CHECK(conv == "00010203040506Z"); + } } } From 9782dda568a18c114742375153fad1e95f8c7207 Mon Sep 17 00:00:00 2001 From: Eddy Ashton Date: Mon, 9 Feb 2026 16:18:42 +0000 Subject: [PATCH 16/17] The robot giveth, the robot taketh away --- .../custom_authorization.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/tests/js-custom-authorization/custom_authorization.py b/tests/js-custom-authorization/custom_authorization.py index c57999e57e0..2c62cc2b880 100644 --- a/tests/js-custom-authorization/custom_authorization.py +++ b/tests/js-custom-authorization/custom_authorization.py @@ -20,9 +20,12 @@ from http import HTTPStatus import subprocess from contextlib import contextmanager +from functools import partial from loguru import logger as LOG +utctime = partial(datetime.datetime, tzinfo=datetime.UTC) + @reqs.description("Test custom authorization") def test_custom_auth(network, args): @@ -306,8 +309,7 @@ def create_keypair(local_id, valid_from, validity_days): local_user_id = "in_the_past" create_keypair( local_user_id, - datetime.datetime.now(timezone=datetime.timezone.utc) - - datetime.timedelta(days=50), + datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=50), 3, ) network.consortium.add_user(primary, local_user_id) @@ -321,8 +323,7 @@ def create_keypair(local_id, valid_from, validity_days): local_user_id = "in_the_future" create_keypair( local_user_id, - datetime.datetime.now(timezone=datetime.timezone.utc) - + datetime.timedelta(days=50), + datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=50), 3, ) network.consortium.add_user(primary, local_user_id) @@ -334,9 +335,9 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("No leeway added to cert time evaluation") local_user_id = "just_expired" - valid_from = datetime.datetime.now( - timezone=datetime.timezone.utc - ) - datetime.timedelta(days=1, seconds=2) + valid_from = datetime.datetime.now(datetime.UTC) - datetime.timedelta( + days=1, seconds=2 + ) create_keypair(local_user_id, valid_from, 1) network.consortium.add_user(primary, local_user_id) @@ -347,7 +348,7 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("Long-lived cert doesn't wraparound") local_user_id = "long_lived" - valid_from = datetime.datetime.now(timezone=datetime.timezone.utc) + valid_from = datetime.datetime.now(datetime.UTC) create_keypair(local_user_id, valid_from, 1_000_000) network.consortium.add_user(primary, local_user_id) @@ -358,8 +359,8 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("Future Not-Before doesn't wraparound") local_user_id = "distant_future" # system_clock max representable time is currently 2262-04-11, so use a date after that to check for wraparound - valid_from = datetime.datetime( - year=2262, month=4, day=12, timezone=datetime.timezone.utc + valid_from = utctime( + year=2262, month=4, day=12 ) create_keypair(local_user_id, valid_from, 4) network.consortium.add_user(primary, local_user_id) @@ -376,11 +377,11 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("Representable range") local_user_id = "representable" # Python crypto enforces minimum Not-Before of 1950-01-01 - valid_from = datetime.datetime( - year=1950, month=1, day=1, timezone=datetime.timezone.utc + valid_from = utctime( + year=1950, month=1, day=1 ) # Probe maximum validity range - validity_days = (datetime.datetime(year=9999, month=12, day=31) - valid_from).days + validity_days = (utctime(year=9999, month=12, day=31) - valid_from).days create_keypair(local_user_id, valid_from, validity_days) network.consortium.add_user(primary, local_user_id) From 70e5a5c40ef8c9c7890063a2052bb8170d810199 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Mon, 9 Feb 2026 19:21:54 +0000 Subject: [PATCH 17/17] fmt --- tests/js-custom-authorization/custom_authorization.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/js-custom-authorization/custom_authorization.py b/tests/js-custom-authorization/custom_authorization.py index 2c62cc2b880..e76cd883bde 100644 --- a/tests/js-custom-authorization/custom_authorization.py +++ b/tests/js-custom-authorization/custom_authorization.py @@ -359,9 +359,7 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("Future Not-Before doesn't wraparound") local_user_id = "distant_future" # system_clock max representable time is currently 2262-04-11, so use a date after that to check for wraparound - valid_from = utctime( - year=2262, month=4, day=12 - ) + valid_from = utctime(year=2262, month=4, day=12) create_keypair(local_user_id, valid_from, 4) network.consortium.add_user(primary, local_user_id) @@ -377,9 +375,7 @@ def create_keypair(local_id, valid_from, validity_days): LOG.info("Representable range") local_user_id = "representable" # Python crypto enforces minimum Not-Before of 1950-01-01 - valid_from = utctime( - year=1950, month=1, day=1 - ) + valid_from = utctime(year=1950, month=1, day=1) # Probe maximum validity range validity_days = (utctime(year=9999, month=12, day=31) - valid_from).days create_keypair(local_user_id, valid_from, validity_days)