]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
Add some tests for the OPENSSL_gmtime family of public API.
authorBob Beck <beck@openssl.org>
Wed, 8 Oct 2025 21:11:13 +0000 (15:11 -0600)
committerNeil Horman <nhorman@openssl.org>
Fri, 24 Oct 2025 14:59:04 +0000 (10:59 -0400)
As well as some basic sanity testing for the internal asn1_time
conversion functions.

Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Saša Nedvědický <sashan@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/28949)

crypto/asn1/asn1_local.h
doc/man3/OPENSSL_gmtime.pod
include/crypto/asn1.h
test/asn1_internal_test.c
test/asn1_time_test.c

index f92c1c5a3f973406c34a617968c19b150a13101b..11a54be603bdfbfdf5c08c5e9143f80aea61bffb 100644 (file)
@@ -90,8 +90,6 @@ int ossl_c2i_uint64_int(uint64_t *ret, int *neg, const unsigned char **pp,
                         long len);
 int ossl_i2c_uint64_int(unsigned char *p, uint64_t r, int neg);
 
-ASN1_TIME *ossl_asn1_time_from_tm(ASN1_TIME *s, struct tm *ts, int type);
-
 int ossl_asn1_item_ex_new_intern(ASN1_VALUE **pval, const ASN1_ITEM *it,
                                  OSSL_LIB_CTX *libctx, const char *propq);
 int ossl_asn1_time_time_t_to_tm(const time_t *time, struct tm *out_tm);
index 0c7fd4b80dbd1480a5914aacfd35d1627ea114f1..9df0e2990c9cf633aa47a8944a433dbd7258c4b0 100644 (file)
@@ -20,16 +20,19 @@ OPENSSL_tm_to_posix - platform-agnostic OpenSSL time routines
 
  #include <openssl/posix_time.h>
  int OPENSSL_timegm(const struct tm *tm, time_t *out_time);
- int OPENSSL_posix_to_tm(int64_t time struct tm *out_tm);
- int OPENSSL_tm_to_posix(struct tm t_tm int64_t *out);
+ int OPENSSL_posix_to_tm(int64_t time, struct tm *out_tm);
+ int OPENSSL_tm_to_posix(struct tm t_tm, int64_t *out);
 
 =head1 DESCRIPTION
 
-OPENSSL_gmtime() returns the UTC time specified by I<timer> into the
-provided I<result> argument. |timer| must be in the range of year 0 to
-9999.  Only the fields I<tm_year>, I<tm_mon>, I<tm_mday>, I<tm_hour>,
-I<tm_min>, and I<tm_sec> will be updated  in I<result> on success,
-all other fields will be zeroed.
+OPENSSL_gmtime() returns the Unix/Posix time specified by I<timer>
+into the provided I<result> argument. |timer| must be a value of the
+number of seconds relative to 0 hours, 0 minutes, 0 seconds, January 1,
+1970, Coordinated Universal Time, without including leap seconds, and
+must be in the range of the year 0 to 9999 inclusively. I<result> will
+be zeroed, and only the fields I<tm_year>, I<tm_mon>, I<tm_mday>,
+I<tm_hour>, I<tm_min>, and I<tm_sec> will be updated in I<result> on
+success.
 
 OPENSSL_gmtime_adj() adds the offsets in I<offset_day> and I<offset_sec> to I<tm>.
 
@@ -39,47 +42,73 @@ OPENSSL_timegm() converts a time structure in UTC time in I<tm> to a time_t valu
 I<out_time>.
 
 OPENSSL_posix_to_tm() converts a int64_t POSIX time value in I<time>,
-which must be in the range of year 0 to 9999, to a broken out time
-value in I<tm>. Only the fields I<tm_year>, I<tm_mon>, I<tm_mday>,
-I<tm_hour>, I<tm_min>, and I<tm_sec> will be updated in I<tm>
-on success, all other fields will be zeroed.
+to a broken out time value in I<tm>. I<time> must be a value of the
+number of seconds relative to 0 hours, 0 minutes, 0 seconds, January 1,
+1970, Coordinated Universal Time, without including leap seconds, and
+must be in the range of the year 0 to 9999 inclusively. I<tm> will be
+zeroed, and only the fields I<tm_year>, I<tm_mon>, I<tm_mday>,
+I<tm_hour>, I<tm_min>, and I<tm_sec> will be updated in I<tm> on
+success.
 
-OPENSSL_tm_to_posix() converts a time value between the years 0 and 9999 in I<tm>
-to a POSIX time value in I<out>.
+OPENSSL_tm_to_posix() converts a time value between the years 0 and
+9999 in I<tm> to a POSIX time value in I<out>.
 
 =head1 NOTES
 
-It is an error to call OPENSSL_gmtime() with I<result> equal to NULL. The
-contents of the time_t given by I<timer> are stored into the I<result>. Calling
-with I<timer> equal to NULL means use the current time.
+These functions are intended for use when using I<struct tm> values
+converted from ASN1_TIME values. They are not intended for general
+purpose time conversions, and should not be used for other purposes.
 
-OPENSSL_gmtime_adj() converts I<tm> into a days and seconds value, adds the
-offsets, then converts back into a I<struct tm> specified by I<tm>. Leap seconds
-are not considered.
+When these functions use a I<struct tm> as input, they consider only
+the I<tm_year>, I<tm_mon>, I<tm_mday>, I<tm_hour>, I<tm_min>, and
+I<tm_sec> fields of the structure, all other fields are ignored.
+Out of range values, including leap seconds, are not permitted.
 
-OPENSSL_gmtime_diff() calculates the difference between the two I<struct tm>
-structures I<from> and I<to>. The difference in days is placed into I<*pday>,
-the remaining seconds are placed to I<*psec>. The value in I<*psec> will be less
-than the number of seconds per day (3600). Leap seconds are not considered.
+OPENSSL_gmtime_adj() converts I<tm> into a days and seconds value,
+adds I<offset_day> and I<offset_sec> to the values, and then converts
+back into a I<struct tm> specified by I<tm>.
+
+It is an error to call OPENSSL_gmtime() with I<result> equal to
+NULL. The contents of the time_t given by I<timer> are stored into the
+I<result>. Calling with I<timer> equal to NULL means use the current
+time.
+
+OPENSSL_gmtime_adj() converts I<tm> into a days and seconds value,
+adds the offsets, then converts back into a I<struct tm> specified by
+I<tm>.
+
+OPENSSL_gmtime_diff() calculates the difference between the two
+I<struct tm> structures I<from> and I<to>. The difference in days is
+placed into I<*pday>, the remaining seconds are placed to
+I<*psec>. The value in I<*psec> will be less than the number of
+seconds per day (86400).
 
 =head1 RETURN VALUES
 
-OPENSSL_gmtime returns I<result> on success or NULL for failure.
-It can fail if the time is not representable in a struct tm,
-or if the year is less than 0 or more than 9999.
+OPENSSL_gmtime() returns I<result> on success or NULL for failure. It
+can fail if the input time is outside of the range of year 0 to 9999.
 
-OPENSSL_timegm() returns 1 for success or 0 for failure.
-It can fail if the time is not representable in a time_t,
-or if the year is less than 0 or more than 9999.
+OPENSSL_posix_to_tm() return 1 for success or 0 on failure. It
+can fail if the input time is outside of the range of year 0 to 9999.
 
-OPENSSL_posix_to_tm() and OPENSSL_tm_to_posix()
-return 1 for success or 0 on failure.
-It is a failure if the year is less than 0 or more than 9999.
+OPENSSL_timegm() and OPENSSL_tm_to_posix() return 1 for success or 0 for
+failure.  OpenSSL_timegm() can fail it the time is not representable
+in a time_t, and both may fail on invalid or out of range input in the
+input tm.
 
 OPENSSL_gmtime_adj() and OPENSSL_gmtime_diff() return 0 on error, and 1 on success.
 
+=head1 BUGS
+
+OPENSSL_gmtime() and OPENSSL_timegm() are not platform-agnostic due
+to possible limitations in the range of the platform I<time_t>
+type. OPENSSL_posix_to_tm() and OPENSSL_tm_to_posix() provide similar
+functionality in a platform independent manner.
+
 =head1 HISTORY
 
+OPENSSL_gmtime() returns NULL on error, or I<result> on success.
+
 OPENSSL_gmtime(), OPENSSL_gmtime_adj() and OPENSSL_gmtime_diff() have been
 in OpenSSL since 1.0.0.
 OPENSSL_timegm(), OPENSSL_posix_to_tm() and OPENSSL_tm_to_posix() were originally
index 812ee6701f252f7277953be11005221ff6ffbd9e..be1e4c38630f9606b21fcb81422fce12904b5356 100644 (file)
@@ -154,4 +154,6 @@ int asn1_item_embed_d2i(ASN1_VALUE **pval, const unsigned char **in,
                         char opt, ASN1_TLC *ctx, int depth,
                         OSSL_LIB_CTX *libctx, const char *propq);
 
+ASN1_TIME *ossl_asn1_time_from_tm(ASN1_TIME *s, struct tm *ts, int type);
+
 #endif /* ndef OSSL_CRYPTO_ASN1_H */
index bca6d3f5c8aecc2c8699c5545c042153d133cc4e..d4a2bb10ba7e4ddadefed45e2c791da13c23327b 100644 (file)
@@ -220,6 +220,94 @@ static int test_obj_create_once(const char *oid, const char *sn, const char *ln)
     return 1;
 }
 
+static int test_asn1_time_conversion(char *time_string, const char *file,
+                                     int line)
+{
+    int ret = 0;
+    int is_utc = 0;
+    struct tm tm;
+    ASN1_TIME *asn1_time = NULL;
+    ASN1_TIME *result = NULL;
+
+    if (!TEST_ptr(asn1_time = ASN1_TIME_new()))
+        goto err;
+    if (!TEST_true(ASN1_TIME_set_string(asn1_time, time_string)))
+        goto err;
+    if (!TEST_true(ASN1_TIME_to_tm(asn1_time, &tm)))
+        goto err;
+    is_utc = (tm.tm_year >= 50 && tm.tm_year <= 1949);
+    if (is_utc) {
+        /* our original string could have been provided as gen, or utc */
+        if (strlen(time_string) == 13) {
+            if (!TEST_ptr(result = ossl_asn1_time_from_tm(result, &tm,
+                                                          V_ASN1_UNDEF)))
+                goto err;
+        } else {
+            if (!TEST_ptr(result
+                          = ossl_asn1_time_from_tm(result, &tm,
+                                                   V_ASN1_GENERALIZEDTIME)))
+                goto err;
+        }
+    } else {
+        if (!TEST_ptr(result
+                      = ossl_asn1_time_from_tm(result, &tm,
+                                               V_ASN1_GENERALIZEDTIME)))
+            goto err;
+    }
+    if (!TEST_true((strcmp(time_string,
+                           (const char *)ASN1_STRING_get0_data(result))
+                    == 0))) {
+        TEST_info("Expected time: %s, Got time: %s\n", time_string,
+                  ASN1_STRING_get0_data(result));
+        goto err;
+    }
+
+    ret = 1;
+
+ err:
+    if (!ret)
+        TEST_info("Failed to convert time %s at %s:%d\n", time_string, file, line);
+    ASN1_STRING_free(asn1_time);
+    ASN1_STRING_free(result);
+    return ret;
+}
+
+/*
+ * These should not go through time_t so they should work on any
+ * platform.
+ */
+static int test_asn1_time_tm_conversions(void)
+{
+    int fails = 0;
+
+    fails += !test_asn1_time_conversion("00000101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("10000101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("16001231235959Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("16010101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("700101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("19691231235959Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("691231235959Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("19700101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("700101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("20000101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("20380119031407Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("20380119031408Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("20380119031409Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("21060207062814Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("21060207062815Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("21060207062816Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("30000101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("40000101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("50000101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("60000101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("70000101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("80000101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("90000101000000Z", __FILE__, __LINE__);
+    fails += !test_asn1_time_conversion("99991231235959Z", __FILE__, __LINE__);
+
+    return fails == 0;
+}
+
 static int test_obj_create(void)
 {
 /* Stolen from evp_extra_test.c */
@@ -466,5 +554,6 @@ int setup_tests(void)
     ADD_TEST(test_obj_create);
     ADD_TEST(test_obj_nid_undef);
     ADD_TEST(posix_time_test);
+    ADD_TEST(test_asn1_time_tm_conversions);
     return 1;
 }
index 2c2ef4ffd51d9c738cde6272aa1d8e3b574547f7..776a1cae482c34725ef1694b245f37bced11ded2 100644 (file)
@@ -9,7 +9,9 @@
 
 /* Time tests for the asn1 module */
 
+#include <inttypes.h>
 #include <limits.h>
+#include <stdint.h>
 #include <stdio.h>
 #include <string.h>
 
@@ -18,6 +20,7 @@
 #include <openssl/evp.h>
 #include <openssl/objects.h>
 #include "testutil.h"
+#include "internal/deprecated.h"
 #include "internal/nelem.h"
 
 struct testdata {
@@ -505,6 +508,614 @@ static int convert_asn1_to_time_t(int idx)
     return 1;
 }
 
+/* 0000-01-01 00:00:00 UTC */
+#define MIN_POSIX_TIME INT64_C(-62167219200)
+/* 9999-12-31 23:59:59 UTC */
+#define MAX_POSIX_TIME INT64_C(253402300799)
+#define SECS_PER_HOUR  INT64_C(3600)
+#define SECS_PER_DAY (INT64_C(24) * SECS_PER_HOUR)
+
+static int test_gmtime_diff_limits(void)
+{
+    int ret = 0;
+    int64_t expected_days = (MAX_POSIX_TIME - MIN_POSIX_TIME) / SECS_PER_DAY;
+    int64_t expected_secs = (MAX_POSIX_TIME - MIN_POSIX_TIME) % SECS_PER_DAY;
+    char *min_asn1_time = "00000101000000Z";
+    char *max_asn1_time = "99991231235959Z";
+    struct tm min_tm, max_tm;
+    ASN1_TIME *min = NULL, *max = NULL;
+    int pday, psec, saved_value;
+    int64_t pd, ps;
+
+    if (!TEST_ptr(min = ASN1_STRING_new()))
+        goto err;
+    if (!TEST_ptr(max = ASN1_STRING_new()))
+        goto err;
+    if (!TEST_true(ASN1_TIME_set_string(min, min_asn1_time)))
+        goto err;
+    if (!TEST_true(ASN1_TIME_set_string(max, max_asn1_time)))
+        goto err;
+    if (!TEST_true(ASN1_TIME_to_tm(min, &min_tm)))
+        goto err;
+    if (!TEST_true(ASN1_TIME_to_tm(max, &max_tm)))
+        goto err;
+    if (!TEST_true(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm)))
+        goto err;
+    pd = pday;
+    ps = psec;
+    if (!TEST_int64_t_eq(pd, expected_days))
+        goto err;
+    if (!TEST_int64_t_eq(ps, expected_secs))
+        goto err;
+
+    if (!TEST_true(OPENSSL_gmtime_diff(&pday, &psec, &max_tm, &min_tm)))
+        goto err;
+    pd = pday;
+    ps = psec;
+    if (!TEST_int64_t_eq(pd, - expected_days))
+        goto err;
+    if (!TEST_int64_t_eq(ps, - expected_secs))
+        goto err;
+
+    /*
+     * OPENSSL_gmtime[diff/adj] use struct tm as input to public
+     * API. However unlike most platform provided time conversion
+     * functions, it does not validate the values in the tm as per C99
+     * spec before attempting to use it. It also only partly limits
+     * what it will convert to the ASN1_TIME date range, and does not
+     * support the entire range. The define below makes tests that
+     * fail pass to match the current behaviour.
+     */
+#define ITS_TRADITION_THAT_MAKES_IT_OK
+
+    /*
+     * Struct tm permits second 60 in C99. As neither of the values
+     * tested here are actually a for realsies leap second, (We do not
+     * consult any reference for valid leap seconds from the platform
+     * when doing this with Julian date calculations) it should either
+     * be rejected outright, or possibly discarded and treated as 59.
+     *
+     * As we currently reject a leap second in an ASN1 time, this is
+     * moot when using this in the normal way to compare values from
+     * ASN1_TIME, because we won't accept an ASN1_TIME with a seconds
+     * value of 60.
+     */
+    saved_value = max_tm.tm_sec;
+    max_tm.tm_sec = 60;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        pd = pday;
+        ps = psec;
+        if (!TEST_false((pd == expected_days + 1 && ps == 0))) {
+            TEST_info("OPENSSL_gmtime_diff incorrectly includes bogus leap second");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+            goto err;
+#endif
+        } else {
+            if (!TEST_int64_t_eq(pd, expected_days))
+                goto err;
+            if (!TEST_int64_t_eq(ps, expected_secs))
+                goto err;
+        }
+    }
+    max_tm.tm_sec = saved_value;
+
+    saved_value = min_tm.tm_sec;
+    min_tm.tm_sec = 60;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        pd = pday;
+        ps = psec;
+        if (!TEST_false((pd == expected_days && ps == expected_secs - 60))) {
+            TEST_info("OPENSSL_gmtime_diff incorrectly includes bogus leap second");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+            goto err;
+#endif
+        } else {
+            if (!TEST_int64_t_eq(pd, expected_days))
+                goto err;
+            if (!TEST_int64_t_eq(ps, expected_secs - 59))
+                goto err;
+        }
+    }
+    min_tm.tm_sec = saved_value;
+
+    saved_value = max_tm.tm_sec;
+    max_tm.tm_sec = 60;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        pd = pday;
+        ps = psec;
+        if (!TEST_false((pd == expected_days + 1 && ps == 0))) {
+            TEST_info("OPENSSL_gmtime_diff incorrectly includes bogus leap second");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+            goto err;
+#endif
+        } else {
+            if (!TEST_int64_t_eq(pd, expected_days))
+                goto err;
+            if (!TEST_int64_t_eq(ps, expected_secs))
+                goto err;
+        }
+    }
+    max_tm.tm_sec = saved_value;
+
+    saved_value = min_tm.tm_mon;
+    min_tm.tm_mon = 12;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        TEST_info("OPENSSL_gmtime_diff incorrectly allows month 12");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    min_tm.tm_mon = saved_value;
+
+    saved_value = min_tm.tm_mon;
+    min_tm.tm_mon = -1;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        TEST_info("OPENSSL_gmtime_diff incorrectly allows month -1");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    min_tm.tm_mon = saved_value;
+
+    saved_value = min_tm.tm_mday;
+    min_tm.tm_mday = 32;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        TEST_info("OPENSSL_gmtime_diff incorrectly allows the 32nd of January");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    min_tm.tm_mday = saved_value;
+
+    saved_value = min_tm.tm_mday;
+    min_tm.tm_mday = 0;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        TEST_info("OPENSSL_gmtime_diff incorrectly allows the 0th of January");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    min_tm.tm_mday = saved_value;
+
+    saved_value = min_tm.tm_hour;
+    min_tm.tm_hour = 24;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        TEST_info("OPENSSL_gmtime_diff incorrectly allows hour 24");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    min_tm.tm_hour = saved_value;
+
+    saved_value = min_tm.tm_hour;
+    min_tm.tm_hour = -1;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        TEST_info("OPENSSL_gmtime_diff incorrectly allows hour -1");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    min_tm.tm_hour = saved_value;
+
+    saved_value = min_tm.tm_min;
+    min_tm.tm_min = 60;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        TEST_info("OPENSSL_gmtime_diff incorrectly allows minute 60");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    min_tm.tm_min = saved_value;
+
+    saved_value = min_tm.tm_min;
+    min_tm.tm_min = -1;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        TEST_info("OPENSSL_gmtime_diff incorrectly allows minute -1");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    min_tm.tm_min = saved_value;
+
+    saved_value = min_tm.tm_sec;
+    min_tm.tm_sec = -1;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        TEST_info("OPENSSL_gmtime_diff incorrectly allows second -1");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    min_tm.tm_sec = saved_value;
+
+    saved_value = min_tm.tm_sec;
+    min_tm.tm_sec = 61; /* Not allowed per C99. */
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+        TEST_info("OPENSSL_gmtime_diff incorrectly allows second 61");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    min_tm.tm_sec = saved_value;
+
+    saved_value = min_tm.tm_year;
+    min_tm.tm_year -= 1;
+    /* These should now be outside the asn1 time range. */
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+            goto err;
+    }
+    min_tm.tm_year = saved_value;
+    expected_days -= 365;
+
+    saved_value = max_tm.tm_year;
+    max_tm.tm_year += 1;
+    if (!TEST_false(OPENSSL_gmtime_diff(&pday, &psec, &min_tm, &max_tm))) {
+            goto err;
+    }
+    max_tm.tm_year = saved_value;
+
+    ret = 1;
+
+err:
+    ASN1_STRING_free(min);
+    ASN1_STRING_free(max);
+    return ret;
+}
+
+static int64_t time_t_min(void)
+{
+    time_t t = (time_t) -1;
+
+    if (t > 0) {
+        /* time_t is unsigned 32 bit */
+        OPENSSL_assert(sizeof(time_t) == sizeof(uint32_t));
+        return INT64_C(0);
+    } else {
+#if defined(OPENSSL_SYS_WINDOWS)
+        /* Windows can't handle before 1970, even with signed time_t */
+        return INT64_C(0);
+#else
+        if (sizeof(time_t) == sizeof(int32_t))
+            return (int64_t)INT32_MIN;
+        OPENSSL_assert(sizeof(time_t) == sizeof(uint64_t));
+        return INT64_MIN;
+#endif
+    }
+}
+
+static int64_t time_t_max(void)
+{
+    time_t t = (time_t) -1;
+
+    if (t > 0) {
+        /* time_t is unsigned 32 bit */
+        OPENSSL_assert(sizeof(time_t) == sizeof(uint32_t));
+        return (int64_t)UINT32_MAX;
+    } else {
+        if (sizeof(time_t) == sizeof(int32_t))
+            return (int64_t)INT32_MAX;
+        OPENSSL_assert(sizeof(time_t) == sizeof(uint64_t));
+#if defined(OPENSSL_SYS_WINDOWS)
+       /* Windows can't do past year 3000 even on signed 64 bit time_t */
+        return INT64_C(32535215999);
+#else
+        return INT64_MAX;
+#endif
+    }
+}
+
+static int test_gmtime_range(void)
+{
+    int ret = 0;
+    struct tm tm, copy;
+    time_t tt;
+    int64_t i, platform_min, platform_max;
+    int days;
+    long seconds;
+
+    /*
+     * Ensure that OPENSSL_gmtime() can convert any value
+     * that is both within the range of an ASN1_TIME,
+     * and within the range of the platform time_t
+     */
+    platform_min = time_t_min() < MIN_POSIX_TIME ? MIN_POSIX_TIME : time_t_min();
+    platform_max = time_t_max() > MAX_POSIX_TIME ? MAX_POSIX_TIME : time_t_max();
+
+    for (i = platform_min; i < platform_max; i += 1000000) {
+        tt = (time_t)i;
+        memset(&tm, 0, sizeof(struct tm));
+        if (!TEST_ptr(OPENSSL_gmtime(&tt, &tm))) {
+            TEST_info("OPENSSL_gmtime failed unexpectedly for value %lld", (long long) tt);
+            goto err;
+        }
+    }
+    tt = (time_t)platform_max;
+    if (!TEST_ptr(OPENSSL_gmtime(&tt, &tm))) {
+        TEST_info("OPENSSL_gmtime failed unexpectedly for value %lld", (long long) tt);
+        goto err;
+    }
+
+    if (time_t_min() <= -1) {
+        tt = -1;
+        if (!TEST_ptr(OPENSSL_gmtime(&tt, &tm))) {
+            TEST_info("OPENSSL_gmtime failed unexpectedly for value %lld", (long long) tt);
+            goto err;
+        }
+    }
+    if (time_t_min() <= 0) {
+        tt = 0;
+        if (!TEST_ptr(OPENSSL_gmtime(&tt, &tm))) {
+            TEST_info("OPENSSL_gmtime failed unexpectedly for value %lld", (long long) tt);
+            goto err;
+        }
+    }
+
+    if (time_t_max() >= (int64_t)INT32_MAX) {
+        tt = (time_t)INT32_MAX;
+        if (!TEST_ptr(OPENSSL_gmtime(&tt, &tm))) {
+            TEST_info("OPENSSL_gmtime failed unexpectedly for value %lld", (long long) tt);
+            goto err;
+        }
+    }
+
+    if (time_t_min() >= (int64_t)UINT32_MAX) {
+        tt = (time_t)UINT32_MAX;
+        if (!TEST_ptr(OPENSSL_gmtime(&tt, &tm))) {
+            TEST_info("OPENSSL_gmtime failed unexpectedly for value %lld", (long long) tt);
+            goto err;
+        }
+    }
+
+    /* 00000101000000Z - MIN_POSIX_TIME. */
+    memset(&tm, 0, sizeof(tm));
+    tm.tm_year = - 1900;
+    tm.tm_mday = 1;
+    memcpy(&copy, &tm, sizeof(tm));
+
+    /* Adj is expected to fail for a year less than 0000. */
+    if (!TEST_false(OPENSSL_gmtime_adj(&copy, 0, -1))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly succeeded for year -1");
+        goto err;
+    }
+
+    memcpy(&copy, &tm, sizeof(tm));
+    /* Adj should work for year 0000. */
+    if (!TEST_true(OPENSSL_gmtime_adj(&copy, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly failed for year 0");
+        /* ... except that it doesn't */
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+
+    memcpy(&copy, &tm, sizeof(tm));
+    days = (int) ((MAX_POSIX_TIME - MIN_POSIX_TIME) / SECS_PER_DAY);
+    seconds = (long) ((MAX_POSIX_TIME - MIN_POSIX_TIME) % SECS_PER_DAY);
+    if (!TEST_true(OPENSSL_gmtime_adj(&copy, days, seconds))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly failed for "
+                  "%d days and %ld seconds", - days, - seconds);
+        goto err;
+    }
+    /*
+     * Adj currently fails for result times before 1900, but can
+     * start with a time before that, as long as the result is
+     * before year 10000.
+     */
+    if (!TEST_true(OPENSSL_gmtime_adj(&copy, - days, - seconds))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly failed for "
+                  "%d days and %ld seconds", - days, - seconds);
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+    if (!TEST_mem_eq(&copy, sizeof(copy), &tm, sizeof(tm))) {
+        TEST_info("tm does not have expected value after adj of "
+                  "%d days and %ld seconds", days, seconds);
+        goto err;
+    }
+#endif
+    seconds = (long) ((MAX_POSIX_TIME - MIN_POSIX_TIME) % (int64_t)LONG_MAX);
+    days = (int) ((MAX_POSIX_TIME - MIN_POSIX_TIME
+                   - (int64_t)seconds) / SECS_PER_DAY);
+    if (!TEST_true(OPENSSL_gmtime_adj(&copy, days, seconds))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly failed for "
+                  "%d days and %ld seconds", - days, - seconds);
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    if (!TEST_true(OPENSSL_gmtime_adj(&copy, - days, - seconds))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly failed for "
+                  "%d days and %ld seconds", - days, - seconds);
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+    if (!TEST_mem_eq(&copy, sizeof(copy), &tm, sizeof(tm))) {
+        TEST_info("tm does not have expected value after adj of "
+                  "%d days and %ld seconds", days, seconds);
+        goto err;
+    }
+#endif
+
+    /* 99991231235959Z - MAX_POSIX_TIME. */
+    memset(&tm, 0, sizeof(tm));
+    tm.tm_year = 9999 - 1900;
+    tm.tm_mon = 11;
+    tm.tm_mday = 31;
+    tm.tm_hour = 23;
+    tm.tm_min = 59;
+    tm.tm_sec = 59;
+    memcpy(&copy, &tm, sizeof(tm));
+
+    /* Adjust back to the epoch, and back again. */
+    days = (int) (MAX_POSIX_TIME / SECS_PER_DAY);
+    seconds = (long) (MAX_POSIX_TIME % SECS_PER_DAY);
+    if (!TEST_true(OPENSSL_gmtime_adj(&copy, - days, - seconds))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly failed for "
+                  "%d days and %ld seconds", - days, - seconds);
+        goto err;
+    }
+    if (!TEST_true(OPENSSL_gmtime_adj(&copy, days, seconds))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly failed for "
+                  "%d days and %ld seconds", days, seconds);
+        goto err;
+    }
+    if (!TEST_mem_eq(&copy, sizeof(copy), &tm, sizeof(tm))) {
+        TEST_info("tm does not have expected value after adj of "
+                  "%d days and %ld seconds", days, seconds);
+        goto err;
+    }
+
+    /*
+     * Adjust back to the epoch, and back again using as many
+     * seconds as possible.
+     */
+    seconds = (long) (MAX_POSIX_TIME % (int64_t)LONG_MAX);
+    days = (int) ((MAX_POSIX_TIME - (int64_t)seconds) / SECS_PER_DAY);
+    if (!TEST_true(OPENSSL_gmtime_adj(&copy, - days, - seconds))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly failed for "
+                  "%d days and %ld seconds", - days, - seconds);
+        goto err;
+    }
+    if (!TEST_true(OPENSSL_gmtime_adj(&copy, days, seconds))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly failed for "
+                  "%d days and %ld seconds", days, seconds);
+        goto err;
+    }
+    if (!TEST_mem_eq(&copy, sizeof(copy), &tm, sizeof(tm))) {
+        TEST_info("tm does not have expected value after adj of "
+                  "%d days and %ld seconds", days, seconds);
+        goto err;
+    }
+
+    /* Adj is expected to work for year 9999. */
+    if (!TEST_true(OPENSSL_gmtime_adj(&tm, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly failed");
+        goto err;
+    }
+
+    /* Adj is expected to fail for a year greater than 9999. */
+    if (!TEST_false(OPENSSL_gmtime_adj(&tm, 0, 1))) {
+        TEST_info("OPENSSL_gmtime_adj unexpectedly succeeded");
+        goto err;
+    }
+
+    /* The Epoch */
+    memset(&tm, 0, sizeof(tm));
+    tm.tm_year = 1970 - 1900;
+    tm.tm_mday = 1;
+    memcpy(&copy, &tm, sizeof(tm));
+
+    tm.tm_mon = 12;
+    if (!TEST_false(OPENSSL_gmtime_adj(&tm, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj incorrectly allows month 12");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    tm.tm_mon = -1;
+    if (!TEST_false(OPENSSL_gmtime_adj(&tm, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj incorrectly allows month -1");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    tm.tm_mon = 0;
+
+    tm.tm_mday = 32;
+    if (!TEST_false(OPENSSL_gmtime_adj(&tm, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj incorrectly allows 32nd of January");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    tm.tm_mday = 0;
+    if (!TEST_false(OPENSSL_gmtime_adj(&tm, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj incorrectly allows the 0th of January");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    tm.tm_mday = 1;
+
+    tm.tm_hour = 24;
+    if (!TEST_false(OPENSSL_gmtime_adj(&tm, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj incorrectly allows hour 24");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    tm.tm_hour = -1;
+    if (!TEST_false(OPENSSL_gmtime_adj(&tm, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj incorrectly allows hour -1");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    tm.tm_hour = 0;
+
+    tm.tm_min = 60;
+    if (!TEST_false(OPENSSL_gmtime_adj(&tm, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj incorrectly allows minute 60");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    tm.tm_min = -1;
+    if (!TEST_false(OPENSSL_gmtime_adj(&tm, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj incorrectly allows minute -1");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    tm.tm_min = 0;
+
+    tm.tm_sec = -1;
+    if (!TEST_false(OPENSSL_gmtime_adj(&tm, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj incorrectly allows second -1");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    tm.tm_sec = 61; /* Not allowed per C99. */
+    if (!TEST_false(OPENSSL_gmtime_adj(&tm, 0, 0))) {
+        TEST_info("OPENSSL_gmtime_adj incorrectly allows second 61");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+        goto err;
+#endif
+    }
+    tm.tm_sec = 0;
+
+    /*
+     * 1970 does not have a leap second, although struct tm in C99
+     * allows them to be there.  Since this is a bogus leap second, it
+     * should either be rejected, or should just be ignored and folded
+     * into second 59.
+     *
+     * Note this is moot for us when using tm's converted from an
+     * ASN1_TIME, because we don't allow second 60 in an ASN1_TIME.
+     */
+    tm.tm_sec = 59;
+    memcpy(&copy, &tm, sizeof(tm));
+    copy.tm_sec = 60;
+    if (!TEST_false(OPENSSL_gmtime_adj(&copy, 0, 0))) {
+        if (!TEST_mem_eq(&copy, sizeof(copy), &tm, sizeof(tm))) {
+            TEST_info("OPENSSL_gmtime_adj incorrectly accepted a bogus leap second");
+#if !defined(ITS_TRADITION_THAT_MAKES_IT_OK)
+            goto err;
+#endif
+        }
+    }
+    tm.tm_sec = 0;
+
+    ret = 1;
+
+err:
+    return ret;
+}
+
 /*
  * this test is here to exercise ossl_asn1_time_from_tm
  * with an integer year close to INT_MAX.
@@ -566,5 +1177,7 @@ int setup_tests(void)
     ADD_TEST(test_time_dup);
     ADD_ALL_TESTS(convert_asn1_to_time_t, OSSL_NELEM(asn1_to_utc));
     ADD_TEST(convert_tm_to_asn1_time);
+    ADD_TEST(test_gmtime_diff_limits);
+    ADD_TEST(test_gmtime_range);
     return 1;
 }