diff --git a/Doc/library/ssl.rst b/Doc/library/ssl.rst --- a/Doc/library/ssl.rst +++ b/Doc/library/ssl.rst @@ -359,20 +359,28 @@ IDN A-labels such as ``www*.xn--pthon-kva.org`` are still supported, but ``x*.python.org`` no longer matches ``xn--tda.python.org``. -.. function:: cert_time_to_seconds(timestring) +.. function:: cert_time_to_seconds(cert_time) - Returns a floating-point value containing a normal seconds-after-the-epoch - time value, given the time-string representing the "notBefore" or "notAfter" - date from a certificate. + Return the time in seconds since the Epoch as a floating point + number, given the ``cert_time`` string representing the "notBefore" or + "notAfter" date from a certificate in ``"%b %e %H:%M:%S %Y %Z"`` + strptime format (C locale). Here's an example:: >>> import ssl - >>> ssl.cert_time_to_seconds("May 9 00:00:00 2007 GMT") - 1178694000.0 - >>> import time - >>> time.ctime(ssl.cert_time_to_seconds("May 9 00:00:00 2007 GMT")) - 'Wed May 9 00:00:00 2007' + >>> timestamp = ssl.cert_time_to_seconds("Jan 5 09:34:43 2018 GMT") + >>> timestamp + 1515144883.0 + >>> from datetime import datetime + >>> print(datetime.utcfromtimestamp(timestamp)) + 2018-01-05 09:34:43 + + "notBefore" or "notAfter" dates must use GMT (:rfc:`3280`). + + .. versionchanged:: 3.4 + Interpret the input time as a time in UTC. Local timezone was + used previously. .. function:: get_server_certificate(addr, ssl_version=PROTOCOL_SSLv3, ca_certs=None) diff --git a/Lib/ssl.py b/Lib/ssl.py --- a/Lib/ssl.py +++ b/Lib/ssl.py @@ -852,12 +852,47 @@ # some utility functions def cert_time_to_seconds(cert_time): - """Takes a date-time string in standard ASN1_print form - ("MON DAY 24HOUR:MINUTE:SEC YEAR TIMEZONE") and return - a Python time value in seconds past the epoch.""" + """Return the time in seconds since the Epoch as a floating point + number, given the timestring representing the "notBefore" or + "notAfter" date from a certificate in ``"%b %e %H:%M:%S %Y %Z"`` + strptime format (C locale). - import time - return time.mktime(time.strptime(cert_time, "%b %d %H:%M:%S %Y GMT")) + "notBefore" or "notAfter" dates must use GMT (rfc 3280). + + Month is one of: Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec + (see ASN1_TIME_print()) + """ + import re + from calendar import timegm #NOTE: it brings hardcoded Unix epoch (1970) + + months = [ + "Jan","Feb","Mar","Apr","May","Jun", + "Jul","Aug","Sep","Oct","Nov","Dec" + ] + + # ASN1_TIME_print format: "%s %2d %02d:%02d:%02d %d%s" + m = re.match("(?i)" # case-insensitive for compatibility with strptime() + "(?P{months}) " + "(?P(?: |[1-3])\d) " + "(?P[0-2]\d):" + "(?P[0-6]\d):" + "(?P[0-6]\d) " + "(?P\d+) GMT".format(months="|".join(months)), cert_time) + # NOTE: regex is used instead of time.strptime() to avoid + # dependence on the current locale + if m: + month, *rest = m.groups() + day, hour, minute, second, year = map(int, rest) + #NOTE: avoid datetime constructor to support leap seconds + if (1 <= day <= 31 and 0 <= hour <= 23 and 0 <= minute <= 59 and + 0 <= second <= 61): # use time.strptime() limits + # return a float for compatibility + # (fractional seconds are always zero) + return float(timegm((year, months.index(month.title()) + 1, day, + hour, minute, second))) + # error + raise ValueError('time data %r does not match ' + 'format %r' % (cert_time, "%b %e %H:%M:%S %Y %Z")) PEM_HEADER = "-----BEGIN CERTIFICATE-----" PEM_FOOTER = "-----END CERTIFICATE-----" diff --git a/Lib/test/test_ssl.py b/Lib/test/test_ssl.py --- a/Lib/test/test_ssl.py +++ b/Lib/test/test_ssl.py @@ -10,6 +10,7 @@ import gc import os import errno +import locale import pprint import tempfile import urllib.request @@ -86,6 +87,12 @@ # 0.9.8 or higher return ssl.OPENSSL_VERSION_INFO >= (0, 9, 8, 0, 15) +def utc_offset(): + # local time = utc time + utc offset + if time.daylight and time.localtime().tm_isdst > 0: + return -time.altzone # seconds + return -time.timezone + def asn1time(cert_time): # Some versions of OpenSSL ignore seconds, see #18207 # 0.9.8.i @@ -644,6 +651,72 @@ self.assertEqual(str(cx.exception), "only stream sockets are supported") +class UtilityTests(unittest.TestCase): + + def good(self, timestring, timestamp): + self.assertEqual(ssl.cert_time_to_seconds(timestring), timestamp) + + def bad(self, timestring): + with self.assertRaises(ValueError): + ssl.cert_time_to_seconds(timestring) + + @unittest.skipUnless(utc_offset(), + 'local time needs to be different from UTC') + def test_cert_time_to_seconds_timezone(self): + # Issue #19940: ssl.cert_time_to_seconds() returns wrong + # results if local timezone is not UTC + self.good("May 9 00:00:00 2007 GMT", 1178668800.0) + self.good("Jan 5 09:34:43 2018 GMT", 1515144883.0) + + def test_cert_time_to_seconds(self): + self.good("JaN 5 09:34:43 2018 GmT", 1515144883.0) # case-insensitive + self.bad( "Jan 05 09:34:43 2018 GMT") # not '%e' format for day + self.bad( "Jan 5 09:34 2018 GMT") # no seconds + self.bad( "Jan 5 09:34:43 2018") # no GMT + self.bad( "Jan 5 09:34:43 2018 MSK") # not a GMT timezone + self.bad( "Jan 35 09:34:43 2018 GMT") # invalid day + + newyear_ts = 1230768000.0 + self.good("Dec 31 23:59:60 2008 GMT", newyear_ts) # leap seconds + self.good("Jan 1 00:00:00 2009 GMT", newyear_ts) # same timestamp + + # allow 60th second (even if it is not a leap second) + self.good("Jan 5 09:34:60 2018 GMT", 1515144900) + # allow 2nd leap second for compatibility with time.strptime() + self.good("Jan 5 09:34:61 2018 GMT", 1515144901) + self.bad( "Jan 5 09:34:62 2018 GMT") # invalid seconds + + # no special treatement for the special value: + # 99991231235959Z (rfc 3280) + self.good("Dec 31 23:59:59 9999 GMT", 253402300799.0) + + def test_cert_time_to_seconds_locale(self): + # `cert_time_to_seconds()` should be locale independent + #NOTE: this test modifies a global resource (locale) + + def local_february_name(): + return time.strftime('%b', (1, 2, 3, 4, 5, 6, 0, 0, 0)).lower() + + #NOTE: don't use test.support.run_with_locale() to be able to + # log if the test is skipped + locale_info = locale.getlocale(locale.LC_TIME) + try: + try: + locale.setlocale(locale.LC_TIME, 'pl_PL.utf8') + except locale.Error: + self.skipTest("test needs pl_PL.utf8 locale") + else: + if local_february_name() == 'feb': + self.skipTest("locale-specific month name needs to be " + "different from C locale") + + # locale-independent + self.good( "May 9 00:00:00 2007 GMT", 1178668800.0) + self.bad("Lut 9 00:00:00 2007 GMT") + finally: + locale.setlocale(locale.LC_TIME, locale_info) + + class ContextTests(unittest.TestCase): @skip_if_broken_ubuntu_ssl @@ -2842,7 +2915,7 @@ if not os.path.exists(filename): raise support.TestFailed("Can't read certificate file %r" % filename) - tests = [ContextTests, BasicSocketTests, SSLErrorTests] + tests = [ContextTests, BasicSocketTests, SSLErrorTests, UtilityTests] if support.is_resource_enabled('network'): tests.append(NetworkedTests)