Validate ASN.1 times according to RFC 5280

Refuse to parse times that are invalid according to RFC 5280, with
a few exceptions for compatibility. This can affect test code that
relies on making and parsing certificates that contain invalid times.

Update-Note: Certificates containing invalid ASN.1 times will no longer parse.

Bug: 491, 427

Change-Id: I2a3fe3a4d359ac662340a225d05b360718eb8c29
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/52665
Commit-Queue: Bob Beck <bbe@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/crypto/asn1/a_gentm.c b/crypto/asn1/a_gentm.c
index 3e6f14e..e0f6d39 100644
--- a/crypto/asn1/a_gentm.c
+++ b/crypto/asn1/a_gentm.c
@@ -55,130 +55,26 @@
  * [including the GNU Public Licence.] */
 
 #include <openssl/asn1.h>
+#include <openssl/bytestring.h>
+#include <openssl/err.h>
+#include <openssl/mem.h>
 
 #include <string.h>
 #include <time.h>
 
-#include <openssl/err.h>
-#include <openssl/mem.h>
-
 #include "internal.h"
 
 int asn1_generalizedtime_to_tm(struct tm *tm, const ASN1_GENERALIZEDTIME *d)
 {
-    static const int min[9] = { 0, 0, 1, 1, 0, 0, 0, 0, 0 };
-    static const int max[9] = { 99, 99, 12, 31, 23, 59, 59, 12, 59 };
-    char *a;
-    int n, i, l, o;
-
-    if (d->type != V_ASN1_GENERALIZEDTIME)
-        return (0);
-    l = d->length;
-    a = (char *)d->data;
-    o = 0;
-    /*
-     * GENERALIZEDTIME is similar to UTCTIME except the year is represented
-     * as YYYY. This stuff treats everything as a two digit field so make
-     * first two fields 00 to 99
-     */
-    if (l < 13)
-        goto err;
-    for (i = 0; i < 7; i++) {
-        if ((i == 6) && ((a[o] == 'Z') || (a[o] == '+') || (a[o] == '-'))) {
-            i++;
-            if (tm)
-                tm->tm_sec = 0;
-            break;
-        }
-        if ((a[o] < '0') || (a[o] > '9'))
-            goto err;
-        n = a[o] - '0';
-        if (++o > l)
-            goto err;
-
-        if ((a[o] < '0') || (a[o] > '9'))
-            goto err;
-        n = (n * 10) + a[o] - '0';
-        if (++o > l)
-            goto err;
-
-        if ((n < min[i]) || (n > max[i]))
-            goto err;
-        if (tm) {
-            switch (i) {
-            case 0:
-                tm->tm_year = n * 100 - 1900;
-                break;
-            case 1:
-                tm->tm_year += n;
-                break;
-            case 2:
-                tm->tm_mon = n - 1;
-                break;
-            case 3:
-                tm->tm_mday = n;
-                break;
-            case 4:
-                tm->tm_hour = n;
-                break;
-            case 5:
-                tm->tm_min = n;
-                break;
-            case 6:
-                tm->tm_sec = n;
-                break;
-            }
-        }
+    if (d->type != V_ASN1_GENERALIZEDTIME) {
+        return 0;
     }
-    /*
-     * Optional fractional seconds: decimal point followed by one or more
-     * digits.
-     */
-    if (a[o] == '.') {
-        if (++o > l)
-            goto err;
-        i = o;
-        while ((a[o] >= '0') && (a[o] <= '9') && (o <= l))
-            o++;
-        /* Must have at least one digit after decimal point */
-        if (i == o)
-            goto err;
+    CBS cbs;
+    CBS_init(&cbs, d->data, (size_t)d->length);
+    if (!CBS_parse_generalized_time(&cbs, tm, /*allow_timezone_offset=*/0)) {
+        return 0;
     }
-
-    if (a[o] == 'Z')
-        o++;
-    else if ((a[o] == '+') || (a[o] == '-')) {
-        int offsign = a[o] == '-' ? 1 : -1, offset = 0;
-        o++;
-        if (o + 4 > l)
-            goto err;
-        for (i = 7; i < 9; i++) {
-            if ((a[o] < '0') || (a[o] > '9'))
-                goto err;
-            n = a[o] - '0';
-            o++;
-            if ((a[o] < '0') || (a[o] > '9'))
-                goto err;
-            n = (n * 10) + a[o] - '0';
-            if ((n < min[i]) || (n > max[i]))
-                goto err;
-            if (tm) {
-                if (i == 7)
-                    offset = n * 3600;
-                else if (i == 8)
-                    offset += n * 60;
-            }
-            o++;
-        }
-        if (offset && !OPENSSL_gmtime_adj(tm, 0, offset * offsign))
-            return 0;
-    } else if (a[o]) {
-        /* Missing time zone information. */
-        goto err;
-    }
-    return (o == l);
- err:
-    return (0);
+    return 1;
 }
 
 int ASN1_GENERALIZEDTIME_check(const ASN1_GENERALIZEDTIME *d)
diff --git a/crypto/asn1/a_utctm.c b/crypto/asn1/a_utctm.c
index 21ea2cc..ea984c5 100644
--- a/crypto/asn1/a_utctm.c
+++ b/crypto/asn1/a_utctm.c
@@ -55,106 +55,26 @@
  * [including the GNU Public Licence.] */
 
 #include <openssl/asn1.h>
+#include <openssl/bytestring.h>
+#include <openssl/err.h>
+#include <openssl/mem.h>
 
 #include <string.h>
 #include <time.h>
 
-#include <openssl/err.h>
-#include <openssl/mem.h>
-
 #include "internal.h"
 
-
 int asn1_utctime_to_tm(struct tm *tm, const ASN1_UTCTIME *d)
 {
-    static const int min[8] = { 0, 1, 1, 0, 0, 0, 0, 0 };
-    static const int max[8] = { 99, 12, 31, 23, 59, 59, 12, 59 };
-    char *a;
-    int n, i, l, o;
-
-    if (d->type != V_ASN1_UTCTIME)
-        return (0);
-    l = d->length;
-    a = (char *)d->data;
-    o = 0;
-
-    if (l < 11)
-        goto err;
-    for (i = 0; i < 6; i++) {
-        if ((i == 5) && ((a[o] == 'Z') || (a[o] == '+') || (a[o] == '-'))) {
-            i++;
-            if (tm)
-                tm->tm_sec = 0;
-            break;
-        }
-        if ((a[o] < '0') || (a[o] > '9'))
-            goto err;
-        n = a[o] - '0';
-        if (++o > l)
-            goto err;
-
-        if ((a[o] < '0') || (a[o] > '9'))
-            goto err;
-        n = (n * 10) + a[o] - '0';
-        if (++o > l)
-            goto err;
-
-        if ((n < min[i]) || (n > max[i]))
-            goto err;
-        if (tm) {
-            switch (i) {
-            case 0:
-                tm->tm_year = n < 50 ? n + 100 : n;
-                break;
-            case 1:
-                tm->tm_mon = n - 1;
-                break;
-            case 2:
-                tm->tm_mday = n;
-                break;
-            case 3:
-                tm->tm_hour = n;
-                break;
-            case 4:
-                tm->tm_min = n;
-                break;
-            case 5:
-                tm->tm_sec = n;
-                break;
-            }
-        }
+    if (d->type != V_ASN1_UTCTIME) {
+        return 0;
     }
-    if (a[o] == 'Z')
-        o++;
-    else if ((a[o] == '+') || (a[o] == '-')) {
-        int offsign = a[o] == '-' ? 1 : -1, offset = 0;
-        o++;
-        if (o + 4 > l)
-            goto err;
-        for (i = 6; i < 8; i++) {
-            if ((a[o] < '0') || (a[o] > '9'))
-                goto err;
-            n = a[o] - '0';
-            o++;
-            if ((a[o] < '0') || (a[o] > '9'))
-                goto err;
-            n = (n * 10) + a[o] - '0';
-            if ((n < min[i]) || (n > max[i]))
-                goto err;
-            if (tm) {
-                if (i == 6)
-                    offset = n * 3600;
-                else if (i == 7)
-                    offset += n * 60;
-            }
-            o++;
-        }
-        if (offset && !OPENSSL_gmtime_adj(tm, 0, offset * offsign))
-            return 0;
+    CBS cbs;
+    CBS_init(&cbs, d->data, (size_t)d->length);
+    if (!CBS_parse_utc_time(&cbs, tm, /*allow_timezone_offset=*/1)) {
+        return 0;
     }
-    return o == l;
- err:
-    return 0;
+    return 1;
 }
 
 int ASN1_UTCTIME_check(const ASN1_UTCTIME *d)
diff --git a/crypto/asn1/asn1_test.cc b/crypto/asn1/asn1_test.cc
index 6087ef4..ec6b371 100644
--- a/crypto/asn1/asn1_test.cc
+++ b/crypto/asn1/asn1_test.cc
@@ -901,6 +901,31 @@
                      ASN1_STRING_get0_data(str) + ASN1_STRING_length(str));
 }
 
+static bool ASN1Time_check_time_t(const ASN1_TIME *s, time_t t) {
+  struct tm stm, ttm;
+  int day, sec;
+
+  switch (ASN1_STRING_type(s)) {
+    case V_ASN1_GENERALIZEDTIME:
+      if (!asn1_generalizedtime_to_tm(&stm, s)) {
+        return false;
+      }
+      break;
+    case V_ASN1_UTCTIME:
+      if (!asn1_utctime_to_tm(&stm, s)) {
+        return false;
+      }
+      break;
+    default:
+      return 0;
+  }
+  if (!OPENSSL_gmtime(&t, &ttm) ||
+      !OPENSSL_gmtime_diff(&day, &sec, &ttm, &stm)) {
+    return false;
+  }
+  return day == 0 && sec ==0;
+}
+
 TEST(ASN1Test, SetTime) {
   static const struct {
     time_t time;
@@ -911,6 +936,7 @@
     {-631152000, "19500101000000Z", "500101000000Z"},
     {0, "19700101000000Z", "700101000000Z"},
     {981173106, "20010203040506Z", "010203040506Z"},
+    {951804000, "20000229060000Z", "000229060000Z"},
 #if defined(OPENSSL_64_BIT)
     // TODO(https://crbug.com/boringssl/416): These cases overflow 32-bit
     // |time_t| and do not consistently work on 32-bit platforms. For now,
@@ -939,6 +965,7 @@
       ASSERT_TRUE(utc);
       EXPECT_EQ(V_ASN1_UTCTIME, ASN1_STRING_type(utc.get()));
       EXPECT_EQ(t.utc, ASN1StringToStdString(utc.get()));
+      EXPECT_TRUE(ASN1Time_check_time_t(utc.get(), t.time));
     } else {
       EXPECT_FALSE(utc);
     }
@@ -949,6 +976,7 @@
       ASSERT_TRUE(generalized);
       EXPECT_EQ(V_ASN1_GENERALIZEDTIME, ASN1_STRING_type(generalized.get()));
       EXPECT_EQ(t.generalized, ASN1StringToStdString(generalized.get()));
+      EXPECT_TRUE(ASN1Time_check_time_t(generalized.get(), t.time));
     } else {
       EXPECT_FALSE(generalized);
     }
@@ -963,6 +991,7 @@
         EXPECT_EQ(V_ASN1_GENERALIZEDTIME, ASN1_STRING_type(choice.get()));
         EXPECT_EQ(t.generalized, ASN1StringToStdString(choice.get()));
       }
+      EXPECT_TRUE(ASN1Time_check_time_t(choice.get(), t.time));
     } else {
       EXPECT_FALSE(choice);
     }
diff --git a/crypto/asn1/internal.h b/crypto/asn1/internal.h
index 5bdaac8..29ae206 100644
--- a/crypto/asn1/internal.h
+++ b/crypto/asn1/internal.h
@@ -72,7 +72,7 @@
 /* Wrapper functions for time functions. */
 
 /* OPENSSL_gmtime wraps |gmtime_r|. See the manual page for that function. */
-struct tm *OPENSSL_gmtime(const time_t *time, struct tm *result);
+OPENSSL_EXPORT struct tm *OPENSSL_gmtime(const time_t *time, struct tm *result);
 
 /* OPENSSL_gmtime_adj updates |tm| by adding |offset_day| days and |offset_sec|
  * seconds. */
@@ -81,9 +81,9 @@
 /* OPENSSL_gmtime_diff calculates the difference between |from| and |to| and
  * outputs the difference as a number of days and seconds in |*out_days| and
  * |*out_secs|. */
-int OPENSSL_gmtime_diff(int *out_days, int *out_secs, const struct tm *from,
-                        const struct tm *to);
-
+OPENSSL_EXPORT int OPENSSL_gmtime_diff(int *out_days, int *out_secs,
+                                       const struct tm *from,
+                                       const struct tm *to);
 
 /* Internal ASN1 structures and functions: not for application use */
 
@@ -216,6 +216,9 @@
 OPENSSL_EXPORT void asn1_get_string_table_for_testing(
     const ASN1_STRING_TABLE **out_ptr, size_t *out_len);
 
+OPENSSL_EXPORT int asn1_generalizedtime_to_tm(struct tm *tm,
+                                              const ASN1_GENERALIZEDTIME *d);
+OPENSSL_EXPORT int asn1_utctime_to_tm(struct tm *tm, const ASN1_UTCTIME *d);
 
 #if defined(__cplusplus)
 }  /* extern C */
diff --git a/crypto/asn1/tasn_dec.c b/crypto/asn1/tasn_dec.c
index bb54811..972e89b 100644
--- a/crypto/asn1/tasn_dec.c
+++ b/crypto/asn1/tasn_dec.c
@@ -55,14 +55,14 @@
  * [including the GNU Public Licence.] */
 
 #include <openssl/asn1.h>
+#include <openssl/asn1t.h>
+#include <openssl/bytestring.h>
+#include <openssl/err.h>
+#include <openssl/mem.h>
 
 #include <limits.h>
 #include <string.h>
 
-#include <openssl/asn1t.h>
-#include <openssl/err.h>
-#include <openssl/mem.h>
-
 #include "../internal.h"
 #include "internal.h"
 
@@ -821,6 +821,23 @@
             OPENSSL_PUT_ERROR(ASN1, ASN1_R_UNIVERSALSTRING_IS_WRONG_LENGTH);
             goto err;
         }
+        if (utype == V_ASN1_UTCTIME) {
+            CBS cbs;
+            CBS_init(&cbs, cont, (size_t)len);
+            if (!CBS_parse_utc_time(&cbs, NULL, /*allow_timezone_offset=*/1)) {
+                OPENSSL_PUT_ERROR(ASN1, ASN1_R_INVALID_TIME_FORMAT);
+                goto err;
+            }
+        }
+        if (utype == V_ASN1_GENERALIZEDTIME) {
+            CBS cbs;
+            CBS_init(&cbs, cont, (size_t)len);
+            if (!CBS_parse_generalized_time(&cbs, NULL,
+                                            /*allow_timezone_offset=*/0)) {
+                OPENSSL_PUT_ERROR(ASN1, ASN1_R_INVALID_TIME_FORMAT);
+                goto err;
+            }
+        }
         /* All based on ASN1_STRING and handled the same */
         if (!*pval) {
             stmp = ASN1_STRING_type_new(utype);
diff --git a/crypto/bytestring/bytestring_test.cc b/crypto/bytestring/bytestring_test.cc
index a8c1913..b6b716e 100644
--- a/crypto/bytestring/bytestring_test.cc
+++ b/crypto/bytestring/bytestring_test.cc
@@ -24,9 +24,9 @@
 #include <openssl/crypto.h>
 #include <openssl/span.h>
 
-#include "internal.h"
 #include "../internal.h"
 #include "../test/test_util.h"
+#include "internal.h"
 
 
 TEST(CBSTest, Skip) {
@@ -1493,3 +1493,127 @@
   EXPECT_EQ(4u, cbb_get_utf8_len(0x10000));
   EXPECT_EQ(4u, cbb_get_utf8_len(0x10ffff));
 }
+
+TEST(CBSTest, BogusTime) {
+  static const struct {
+    const char *timestring;
+  } kBogusTimeTests[] = {
+      {""},
+      {"invalidtimesZ"},
+      {"Z"},
+      {"0000"},
+      {"9999Z"},
+      {"00000000000000000000000000000Z"},
+      {"19491231235959"},
+      {"500101000000.001Z"},
+      {"500101000000+6"},
+      {"-1970010100000Z"},
+      {"7a0101000000Z"},
+      {"20500101000000-6"},
+      {"20500101000000.001"},
+      {"20500229000000Z"},
+      {"220229000000Z"},
+      {"20500132000000Z"},
+      {"220132000000Z"},
+      {"20500332000000Z"},
+      {"220332000000Z"},
+      {"20500532000000Z"},
+      {"220532000000Z"},
+      {"20500732000000Z"},
+      {"220732000000Z"},
+      {"20500832000000Z"},
+      {"220832000000Z"},
+      {"20501032000000Z"},
+      {"221032000000Z"},
+      {"20501232000000Z"},
+      {"221232000000Z"},
+      {"20500431000000Z"},
+      {"220431000000Z"},
+      {"20500631000000Z"},
+      {"220631000000Z"},
+      {"20500931000000Z"},
+      {"220931000000Z"},
+      {"20501131000000Z"},
+      {"221131000000Z"},
+      {"20501100000000Z"},
+      {"221100000000Z"},
+      {"19500101000000+0600"},
+  };
+  for (const auto &t : kBogusTimeTests) {
+    SCOPED_TRACE(t.timestring);
+    CBS cbs;
+    CBS_init(&cbs, (const uint8_t *)t.timestring, strlen(t.timestring));
+    EXPECT_FALSE(CBS_parse_generalized_time(&cbs, NULL,
+                                            /*allow_timezone_offset=*/0));
+    EXPECT_FALSE(CBS_parse_utc_time(&cbs, NULL, /*allow_timezone_offset=*/1));
+  }
+  static const struct {
+    const char *timestring;
+  } kUTCTZTests[] = {
+      {"480711220333-0700"},
+      {"140704000000-0700"},
+      {"480222202332-0500"},
+      {"480726113216-0000"},
+      {"480726113216-2359"},
+  };
+  for (const auto &t : kUTCTZTests) {
+    SCOPED_TRACE(t.timestring);
+    CBS cbs;
+    CBS_init(&cbs, (const uint8_t *)t.timestring, strlen(t.timestring));
+    EXPECT_FALSE(CBS_parse_generalized_time(&cbs, NULL,
+                                            /*allow_timezone_offset=*/0));
+    EXPECT_FALSE(CBS_parse_generalized_time(&cbs, NULL,
+                                            /*allow_timezone_offset=*/1));
+    EXPECT_TRUE(CBS_parse_utc_time(&cbs, NULL, /*allow_timezone_offset=*/1));
+    EXPECT_FALSE(CBS_parse_utc_time(&cbs, NULL, /*allow_timezone_offset=*/0));
+  }
+  static const struct {
+    const char *timestring;
+  } kBogusUTCTZTests[] = {
+      {"480711220333-0160"},
+      {"140704000000-9999"},
+      {"480222202332-2400"},
+  };
+  for (const auto &t : kBogusUTCTZTests) {
+    SCOPED_TRACE(t.timestring);
+    CBS cbs;
+    CBS_init(&cbs, (const uint8_t *)t.timestring, strlen(t.timestring));
+    EXPECT_FALSE(CBS_parse_generalized_time(&cbs, NULL,
+                                            /*allow_timezone_offset=*/0));
+    EXPECT_FALSE(CBS_parse_utc_time(&cbs, NULL, /*allow_timezone_offset=*/1));
+  }
+  static const struct {
+    const char *timestring;
+  } kGenTZTests[] = {
+      {"20480711220333-0000"},
+      {"20140704000000-0100"},
+      {"20460311174630-0300"},
+      {"20140704000000-2359"},
+  };
+  for (const auto &t : kGenTZTests) {
+    SCOPED_TRACE(t.timestring);
+    CBS cbs;
+    CBS_init(&cbs, (const uint8_t *)t.timestring, strlen(t.timestring));
+    EXPECT_FALSE(CBS_parse_generalized_time(&cbs, NULL,
+                                            /*allow_timezone_offset=*/0));
+    EXPECT_TRUE(CBS_parse_generalized_time(&cbs, NULL,
+                                           /*allow_timezone_offset=*/1));
+    EXPECT_FALSE(CBS_parse_utc_time(&cbs, NULL, /*allow_timezone_offset=*/1));
+    EXPECT_FALSE(CBS_parse_utc_time(&cbs, NULL, /*allow_timezone_offset=*/0));
+  }
+  static const struct {
+    const char *timestring;
+  } kBogusGenTZTests[] = {
+      {"20480222202332-2400"},
+      {"20140704000000-9999"},
+      {"20480726113216-0160"},
+  };
+  for (const auto &t : kBogusGenTZTests) {
+    SCOPED_TRACE(t.timestring);
+    CBS cbs;
+    CBS_init(&cbs, (const uint8_t *)t.timestring, strlen(t.timestring));
+    EXPECT_FALSE(CBS_parse_generalized_time(&cbs, NULL,
+                                            /*allow_timezone_offset=*/0));
+    EXPECT_FALSE(CBS_parse_utc_time(&cbs, NULL, /*allow_timezone_offset=*/1));
+  }
+}
diff --git a/crypto/bytestring/cbs.c b/crypto/bytestring/cbs.c
index 741ecfb..4e7f379 100644
--- a/crypto/bytestring/cbs.c
+++ b/crypto/bytestring/cbs.c
@@ -12,15 +12,18 @@
  * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
 
-#include <openssl/mem.h>
+#include <openssl/asn1.h>
 #include <openssl/bytestring.h>
+#include <openssl/mem.h>
 
 #include <assert.h>
+#include <ctype.h>
 #include <inttypes.h>
 #include <string.h>
 
-#include "internal.h"
+#include "../asn1/internal.h"
 #include "../internal.h"
+#include "internal.h"
 
 
 void CBS_init(CBS *cbs, const uint8_t *data, size_t len) {
@@ -720,3 +723,161 @@
   CBB_cleanup(&cbb);
   return NULL;
 }
+
+static int cbs_get_two_digits(CBS *cbs, int *out) {
+  uint8_t first_digit, second_digit;
+  if (!CBS_get_u8(cbs, &first_digit)) {
+    return 0;
+  }
+  if (!isdigit(first_digit)) {
+    return 0;
+  }
+  if (!CBS_get_u8(cbs, &second_digit)) {
+    return 0;
+  }
+  if (!isdigit(second_digit)) {
+    return 0;
+  }
+  *out = (first_digit - '0') * 10 + (second_digit - '0');
+  return 1;
+}
+
+static int is_valid_day(int year, int month, int day) {
+  if (day < 1) {
+    return 0;
+  }
+  switch (month) {
+    case 1:
+    case 3:
+    case 5:
+    case 7:
+    case 8:
+    case 10:
+    case 12:
+      return day <= 31;
+    case 4:
+    case 6:
+    case 9:
+    case 11:
+      return day <= 30;
+    case 2:
+      if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) {
+        return day <= 29;
+      } else {
+        return day <= 28;
+      }
+    default:
+      return 0;
+  }
+}
+
+static int CBS_parse_rfc5280_time_internal(const CBS *cbs, int is_gentime,
+                                           int allow_timezone_offset,
+                                           struct tm *out_tm) {
+  int year, month, day, hour, min, sec, tmp;
+  CBS copy = *cbs;
+  uint8_t tz;
+
+  if (is_gentime) {
+    if (!cbs_get_two_digits(&copy, &tmp)) {
+      return 0;
+    }
+    year = tmp * 100;
+    if (!cbs_get_two_digits(&copy, &tmp)) {
+      return 0;
+    }
+      year += tmp;
+  } else {
+    year = 1900;
+    if (!cbs_get_two_digits(&copy, &tmp)) {
+      return 0;
+    }
+    year += tmp;
+    if (year < 1950) {
+      year += 100;
+    }
+    if (year >= 2050) {
+      return 0;  // A Generalized time must be used.
+    }
+  }
+  if (!cbs_get_two_digits(&copy, &month) || month < 1 ||
+      month > 12 ||  // Reject invalid months.
+      !cbs_get_two_digits(&copy, &day) ||
+      !is_valid_day(year, month, day) ||  // Reject invalid days.
+      !cbs_get_two_digits(&copy, &hour) ||
+      hour > 23 ||  // Reject invalid hours.
+      !cbs_get_two_digits(&copy, &min) ||
+      min > 59 ||  // Reject invalid minutes.
+      !cbs_get_two_digits(&copy, &sec) || sec > 59 || !CBS_get_u8(&copy, &tz)) {
+    return 0;
+  }
+
+  int offset_sign = 0;
+  switch (tz) {
+    case 'Z':
+      break;  // We correctly have 'Z' on the end as per spec.
+    case '+':
+      offset_sign = 1;
+      break;  // Should not be allowed per RFC 5280.
+    case '-':
+      offset_sign = -1;
+      break;  // Should not be allowed per RFC 5280.
+    default:
+      return 0;  // Reject anything else after the time.
+  }
+
+  // If allow_timezone_offset is non-zero, allow for a four digit timezone
+  // offset to be specified even though this is not allowed by RFC 5280. We are
+  // permissive of this for UTCTimes due to the unfortunate existence of
+  // artisinally rolled long lived certificates that were baked into places that
+  // are now difficult to change. These certificates were generated with the
+  // 'openssl' command that permissively allowed the creation of certificates
+  // with notBefore and notAfter times specified as strings for direct
+  // certificate inclusion on the command line. For context see cl/237068815.
+  //
+  // TODO(bbe): This has been expunged from public web-pki as the ecosystem has
+  // managed to encourage CA compliance with standards. We should find a way to
+  // get rid of this or make it off by default.
+  int offset_seconds = 0;
+  if (offset_sign != 0) {
+    if (!allow_timezone_offset) {
+      return 0;
+    }
+    int offset_hours, offset_minutes;
+    if (!cbs_get_two_digits(&copy, &offset_hours) ||
+        offset_hours > 23 ||  // Reject invalid hours.
+        !cbs_get_two_digits(&copy, &offset_minutes) ||
+        offset_minutes > 59) {  // Reject invalid minutes.
+      return 0;
+    }
+    offset_seconds = offset_sign * (offset_hours * 3600 + offset_minutes * 60);
+  }
+
+  if (CBS_len(&copy) != 0) {
+    return 0;  // Reject invalid lengths.
+  }
+
+  if (out_tm != NULL) {
+    // Fill in the tm fields corresponding to what we validated.
+    out_tm->tm_year = year - 1900;
+    out_tm->tm_mon = month - 1;
+    out_tm->tm_mday = day;
+    out_tm->tm_hour = hour;
+    out_tm->tm_min = min;
+    out_tm->tm_sec = sec;
+    if (offset_seconds && !OPENSSL_gmtime_adj(out_tm, 0, offset_seconds)) {
+      return 0;
+    }
+  }
+  return 1;
+}
+
+int CBS_parse_generalized_time(const CBS *cbs, struct tm *out_tm,
+                               int allow_timezone_offset) {
+  return CBS_parse_rfc5280_time_internal(cbs, 1, allow_timezone_offset, out_tm);
+}
+
+int CBS_parse_utc_time(const CBS *cbs, struct tm *out_tm,
+                       int allow_timezone_offset) {
+  return CBS_parse_rfc5280_time_internal(cbs, 0, allow_timezone_offset, out_tm);
+}
diff --git a/include/openssl/bytestring.h b/include/openssl/bytestring.h
index 68c1ba4..846ab24 100644
--- a/include/openssl/bytestring.h
+++ b/include/openssl/bytestring.h
@@ -18,6 +18,7 @@
 #include <openssl/base.h>
 
 #include <openssl/span.h>
+#include <time.h>
 
 #if defined(__cplusplus)
 extern "C" {
@@ -353,6 +354,25 @@
 OPENSSL_EXPORT char *CBS_asn1_oid_to_text(const CBS *cbs);
 
 
+// CBS_parse_generalized_time returns one if |cbs| is a valid DER-encoded, ASN.1
+// GeneralizedTime body within the limitations imposed by RFC 5280, or zero
+// otherwise. If |allow_timezone_offset| is non-zero, four-digit timezone
+// offsets, which would not be allowed by DER, are permitted. On success, if
+// |out_tm| is non-NULL, |*out_tm| will be zeroed, and then set to the
+// corresponding time in UTC. This function does not compute |out_tm->tm_wday|
+// or |out_tm->tm_yday|.
+OPENSSL_EXPORT int CBS_parse_generalized_time(const CBS *cbs, struct tm *out_tm,
+                                              int allow_timezeone_offset);
+
+// CBS_parse_utc_time returns one if |cbs| is a valid DER-encoded, ASN.1
+// UTCTime body within the limitations imposed by RFC 5280, or zero otherwise.
+// If |allow_timezone_offset| is non-zero, four-digit timezone offsets, which
+// would not be allowed by DER, are permitted. On success, if |out_tm| is
+// non-NULL, |*out_tm| will be zeroed, and then set to the corresponding time
+// in UTC. This function does not compute |out_tm->tm_wday| or |out_tm->tm_yday|.
+OPENSSL_EXPORT int CBS_parse_utc_time(const CBS *cbs, struct tm *out_tm,
+                                      int allow_timezeone_offset);
+
 // CRYPTO ByteBuilder.
 //
 // |CBB| objects allow one to build length-prefixed serialisations. A |CBB|