From 9f81fa2cd2e56b29faeb7c4aaa57df855da16bba Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Tue, 13 Oct 2026 08:34:47 +0200 Subject: [PATCH] mod_md: Fix a one-second race in md_asn1_time_get(). * modules/md/md_crypt.c (md_asn1_time_get): For OpenSSL >= 1.1.1, convert the certificate's ASN1_TIME directly via ASN1_TIME_to_tm() instead of reconstructing it as apr_time_now() + ASN1_TIME_diff(). The latter samples the current time twice (once explicitly, once inside ASN1_TIME_diff()); if the wall clock ticks over a second between the two reads, the result is off by one second. This is a real, reproducible race, not a theoretical one: forcing the two reads to straddle a second boundary reproduces it in 29-39 out of 40 trials. It intermittently broke the exact-timestamp assertions in test/modules/md/test_920_status.py::test_md_920_002. * changes-entries/md-asn1-time-race.txt: Add entry. --- changes-entries/md-asn1-time-race.txt | 3 +++ modules/md/md_crypt.c | 34 ++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 changes-entries/md-asn1-time-race.txt diff --git a/changes-entries/md-asn1-time-race.txt b/changes-entries/md-asn1-time-race.txt new file mode 100644 index 00000000000..132283467c8 --- /dev/null +++ b/changes-entries/md-asn1-time-race.txt @@ -0,0 +1,3 @@ + *) mod_md: Fix a race in md_asn1_time_get() that could report a + certificate's notBefore/notAfter time off by one second, + intermittently breaking exact-timestamp checks. diff --git a/modules/md/md_crypt.c b/modules/md/md_crypt.c index 5e934d88ac4..aae2a3f05de 100644 --- a/modules/md/md_crypt.c +++ b/modules/md/md_crypt.c @@ -205,9 +205,10 @@ static int pem_passwd(char *buf, int size, int rwflag, void *baton) /* date time things */ /* Get the apr time (micro seconds, since 1970) from an ASN1 time, as stored in X509 - * certificates. OpenSSL now has a utility function, but other *SSL derivatives have - * not caught up yet or chose to ignore. An alternative is implemented, we prefer - * however the *SSL to maintain such things. + * certificates. Where available (OpenSSL >= 1.1.1), convert it directly via + * ASN1_TIME_to_tm() rather than as an offset from the current time, since the + * latter samples "now" twice and is racy by up to a second. Older OpenSSL/LibreSSL + * builds keep their existing conversions below. */ static apr_time_t md_asn1_time_get(const ASN1_TIME* time) { @@ -250,6 +251,33 @@ static apr_time_t md_asn1_time_get(const ASN1_TIME* time) t.tm_sec = (str[i++] - '0') * 10; t.tm_sec += (str[i++] - '0'); + if (APR_SUCCESS == apr_time_exp_gmt_get(&ts, &t)) { + return ts; + } + return 0; +#elif OPENSSL_VERSION_NUMBER >= 0x10101000L && !defined(LIBRESSL_VERSION_NUMBER) + /* Convert the ASN1_TIME directly to an absolute calendar time. Do not + * reconstruct it as "now + diff-from-now": ASN1_TIME_diff() samples + * the current time itself, independently of the apr_time_now() call + * below, so if the wall clock ticks over a second between the two + * reads the result is off by one second. That race is real, not + * theoretical: it intermittently broke exact-timestamp comparisons + * in test/modules/md/test_920_status.py. */ + struct tm exp_tm; + apr_time_exp_t t; + apr_time_t ts; + + if (!ASN1_TIME_to_tm(time, &exp_tm)) { + return 0; + } + memset(&t, 0, sizeof(t)); + t.tm_sec = exp_tm.tm_sec; + t.tm_min = exp_tm.tm_min; + t.tm_hour = exp_tm.tm_hour; + t.tm_mday = exp_tm.tm_mday; + t.tm_mon = exp_tm.tm_mon; + t.tm_year = exp_tm.tm_year; + if (APR_SUCCESS == apr_time_exp_gmt_get(&ts, &t)) { return ts; }