From 20a4c3dc4311aaf7b17504eaaf877c84b8c90850 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Wed, 23 Oct 2024 20:44:50 +0200 Subject: [PATCH 1/2] Allow abstract backend class to handle both with and without timezone. --- .../module_utils/acme/backend_cryptography.py | 39 +------------------ plugins/module_utils/acme/backends.py | 21 ++++++---- 2 files changed, 14 insertions(+), 46 deletions(-) diff --git a/plugins/module_utils/acme/backend_cryptography.py b/plugins/module_utils/acme/backend_cryptography.py index b652240dc..5a6946e54 100644 --- a/plugins/module_utils/acme/backend_cryptography.py +++ b/plugins/module_utils/acme/backend_cryptography.py @@ -11,7 +11,6 @@ import base64 import binascii -import datetime import os import traceback @@ -22,7 +21,6 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.backends import ( CertificateInformation, CryptoBackend, - _parse_acme_timestamp, ) from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import ( @@ -38,10 +36,6 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64 -from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( - OpenSSLObjectError, -) - from ansible_collections.community.crypto.plugins.module_utils.crypto.math import ( convert_int_to_bytes, convert_int_to_hex, @@ -65,11 +59,6 @@ from ansible_collections.community.crypto.plugins.module_utils.time import ( ensure_utc_timezone, - from_epoch_seconds, - get_epoch_seconds, - get_now_datetime, - get_relative_time_option, - UTC, ) CRYPTOGRAPHY_MINIMAL_VERSION = '1.5' @@ -184,33 +173,7 @@ def match(self, certificate): class CryptographyBackend(CryptoBackend): def __init__(self, module): - super(CryptographyBackend, self).__init__(module) - - def get_now(self): - return get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE) - - def parse_acme_timestamp(self, timestamp_str): - return _parse_acme_timestamp(timestamp_str, with_timezone=CRYPTOGRAPHY_TIMEZONE) - - def parse_module_parameter(self, value, name): - try: - return get_relative_time_option(value, name, backend='cryptography', with_timezone=CRYPTOGRAPHY_TIMEZONE) - except OpenSSLObjectError as exc: - raise BackendException(to_native(exc)) - - def interpolate_timestamp(self, timestamp_start, timestamp_end, percentage): - start = get_epoch_seconds(timestamp_start) - end = get_epoch_seconds(timestamp_end) - return from_epoch_seconds(start + percentage * (end - start), with_timezone=CRYPTOGRAPHY_TIMEZONE) - - def get_utc_datetime(self, *args, **kwargs): - kwargs_ext = dict(kwargs) - if CRYPTOGRAPHY_TIMEZONE and ('tzinfo' not in kwargs_ext and len(args) < 8): - kwargs_ext['tzinfo'] = UTC - result = datetime.datetime(*args, **kwargs_ext) - if CRYPTOGRAPHY_TIMEZONE and ('tzinfo' in kwargs or len(args) >= 8): - result = ensure_utc_timezone(result) - return result + super(CryptographyBackend, self).__init__(module, with_timezone=CRYPTOGRAPHY_TIMEZONE) def parse_key(self, key_file=None, key_content=None, passphrase=None): ''' diff --git a/plugins/module_utils/acme/backends.py b/plugins/module_utils/acme/backends.py index 7c08fae95..cade53093 100644 --- a/plugins/module_utils/acme/backends.py +++ b/plugins/module_utils/acme/backends.py @@ -32,6 +32,7 @@ get_now_datetime, get_relative_time_option, remove_timezone, + UTC, ) @@ -85,31 +86,35 @@ def _parse_acme_timestamp(timestamp_str, with_timezone): @six.add_metaclass(abc.ABCMeta) class CryptoBackend(object): - def __init__(self, module): + def __init__(self, module, with_timezone=False): self.module = module + self._with_timezone = with_timezone def get_now(self): - return get_now_datetime(with_timezone=False) + return get_now_datetime(with_timezone=self._with_timezone) def parse_acme_timestamp(self, timestamp_str): # RFC 3339 (https://www.rfc-editor.org/info/rfc3339) - return _parse_acme_timestamp(timestamp_str, with_timezone=False) + return _parse_acme_timestamp(timestamp_str, with_timezone=self._with_timezone) def parse_module_parameter(self, value, name): try: - return get_relative_time_option(value, name, backend='cryptography', with_timezone=False) + return get_relative_time_option(value, name, backend='cryptography', with_timezone=self._with_timezone) except OpenSSLObjectError as exc: raise BackendException(to_native(exc)) def interpolate_timestamp(self, timestamp_start, timestamp_end, percentage): start = get_epoch_seconds(timestamp_start) end = get_epoch_seconds(timestamp_end) - return from_epoch_seconds(start + percentage * (end - start), with_timezone=False) + return from_epoch_seconds(start + percentage * (end - start), with_timezone=self._with_timezone) def get_utc_datetime(self, *args, **kwargs): - result = datetime.datetime(*args, **kwargs) - if 'tzinfo' in kwargs or len(args) >= 8: - result = remove_timezone(result) + kwargs_ext = dict(kwargs) + if self._with_timezone and ('tzinfo' not in kwargs_ext and len(args) < 8): + kwargs_ext['tzinfo'] = UTC + result = datetime.datetime(*args, **kwargs_ext) + if self._with_timezone and ('tzinfo' in kwargs or len(args) >= 8): + result = ensure_utc_timezone(result) return result @abc.abstractmethod From 11598cb1299b60c75ccb48b2bd8260d4d14a63e1 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Wed, 23 Oct 2024 21:21:53 +0200 Subject: [PATCH 2/2] Explicitly use UTC timezone in OpenSSL backend code. --- changelogs/fragments/811-openssl-timezone.yml | 2 ++ .../module_utils/acme/backend_cryptography.py | 6 ++--- .../module_utils/acme/backend_openssl_cli.py | 14 +++++++++--- .../acme/test_backend_cryptography.py | 11 +++++----- .../acme/test_backend_openssl_cli.py | 22 +++++++++++++++---- 5 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 changelogs/fragments/811-openssl-timezone.yml diff --git a/changelogs/fragments/811-openssl-timezone.yml b/changelogs/fragments/811-openssl-timezone.yml new file mode 100644 index 000000000..a30c3f964 --- /dev/null +++ b/changelogs/fragments/811-openssl-timezone.yml @@ -0,0 +1,2 @@ +bugfixes: + - "acme_* modules - when using the OpenSSL backend, explicitly use the UTC timezone in Python code (https://github.com/ansible-collections/community.crypto/pull/811)." diff --git a/plugins/module_utils/acme/backend_cryptography.py b/plugins/module_utils/acme/backend_cryptography.py index 5a6946e54..268bb2a79 100644 --- a/plugins/module_utils/acme/backend_cryptography.py +++ b/plugins/module_utils/acme/backend_cryptography.py @@ -58,7 +58,7 @@ ) from ansible_collections.community.crypto.plugins.module_utils.time import ( - ensure_utc_timezone, + add_or_remove_timezone, ) CRYPTOGRAPHY_MINIMAL_VERSION = '1.5' @@ -382,8 +382,8 @@ def get_cert_days(self, cert_filename=None, cert_content=None, now=None): if now is None: now = self.get_now() - elif CRYPTOGRAPHY_TIMEZONE: - now = ensure_utc_timezone(now) + else: + now = add_or_remove_timezone(now, with_timezone=CRYPTOGRAPHY_TIMEZONE) return (get_not_valid_after(cert) - now).days def create_chain_matcher(self, criterium): diff --git a/plugins/module_utils/acme/backend_openssl_cli.py b/plugins/module_utils/acme/backend_openssl_cli.py index 9aab187ac..eff6af1f4 100644 --- a/plugins/module_utils/acme/backend_openssl_cli.py +++ b/plugins/module_utils/acme/backend_openssl_cli.py @@ -33,6 +33,8 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.math import convert_bytes_to_int +from ansible_collections.community.crypto.plugins.module_utils.time import ensure_utc_timezone + try: import ipaddress except ImportError: @@ -45,7 +47,11 @@ def _extract_date(out_text, name, cert_filename_suffix=""): try: date_str = re.search(r"\s+%s\s*:\s+(.*)" % name, out_text).group(1) - return datetime.datetime.strptime(date_str, '%b %d %H:%M:%S %Y %Z') + # For some reason Python's strptime() doesn't return any timezone information, + # even though the information is there and a supported timezone for all supported + # Python implementations (GMT). So we have to modify the datetime object by + # replacing it by UTC. + return ensure_utc_timezone(datetime.datetime.strptime(date_str, '%b %d %H:%M:%S %Y %Z')) except AttributeError: raise BackendException("No '{0}' date found{1}".format(name, cert_filename_suffix)) except ValueError as exc: @@ -71,7 +77,7 @@ def _extract_octets(out_text, name, required=True, potential_prefixes=None): class OpenSSLCLIBackend(CryptoBackend): def __init__(self, module, openssl_binary=None): - super(OpenSSLCLIBackend, self).__init__(module) + super(OpenSSLCLIBackend, self).__init__(module, with_timezone=True) if openssl_binary is None: openssl_binary = module.get_bin_path('openssl', True) self.openssl_binary = openssl_binary @@ -340,7 +346,9 @@ def get_cert_days(self, cert_filename=None, cert_content=None, now=None): out_text = to_text(out, errors='surrogate_or_strict') not_after = _extract_date(out_text, 'Not After', cert_filename_suffix=cert_filename_suffix) if now is None: - now = datetime.datetime.now() + now = self.get_now() + else: + now = ensure_utc_timezone(now) return (not_after - now).days def create_chain_matcher(self, criterium): diff --git a/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py b/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py index dbd5e02f7..816acf25b 100644 --- a/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py +++ b/tests/unit/plugins/module_utils/acme/test_backend_cryptography.py @@ -12,21 +12,20 @@ from ansible_collections.community.crypto.tests.unit.compat.mock import MagicMock -from ansible_collections.community.crypto.plugins.module_utils.time import UTC - from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import ( HAS_CURRENT_CRYPTOGRAPHY, CryptographyBackend, ) -from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( - ensure_utc_timezone, -) - from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( CRYPTOGRAPHY_TIMEZONE, ) +from ansible_collections.community.crypto.plugins.module_utils.time import ( + ensure_utc_timezone, + UTC, +) + from .backend_data import ( TEST_KEYS, TEST_CSRS, diff --git a/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py b/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py index 8cc1bd829..100205830 100644 --- a/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py +++ b/tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py @@ -17,6 +17,11 @@ OpenSSLCLIBackend, ) +from ansible_collections.community.crypto.plugins.module_utils.time import ( + ensure_utc_timezone, + UTC, +) + from .backend_data import ( TEST_KEYS, TEST_CSRS, @@ -28,7 +33,7 @@ TEST_INTERPOLATE_TIMESTAMP, ) -from ..test_time import TIMEZONES +# from ..test_time import TIMEZONES TEST_IPS = [ @@ -94,20 +99,29 @@ def test_get_cert_information(cert_content, expected_cert_info, openssl_output, module = MagicMock() module.run_command = MagicMock(return_value=(0, openssl_output, 0)) backend = OpenSSLCLIBackend(module, openssl_binary='openssl') + + expected_cert_info = expected_cert_info._replace( + not_valid_after=ensure_utc_timezone(expected_cert_info.not_valid_after), + not_valid_before=ensure_utc_timezone(expected_cert_info.not_valid_before), + ) + cert_info = backend.get_cert_information(cert_filename=str(fn)) assert cert_info == expected_cert_info cert_info = backend.get_cert_information(cert_content=cert_content) assert cert_info == expected_cert_info -@pytest.mark.parametrize("timezone", TIMEZONES) +# @pytest.mark.parametrize("timezone", TIMEZONES) +# Due to a bug in freezegun (https://github.com/spulec/freezegun/issues/348, https://github.com/spulec/freezegun/issues/553) +# this only works with timezone = UTC if CRYPTOGRAPHY_TIMEZONE is truish +@pytest.mark.parametrize("timezone", [datetime.timedelta(hours=0)]) def test_now(timezone): with freeze_time("2024-02-03 04:05:06", tz_offset=timezone): module = MagicMock() backend = OpenSSLCLIBackend(module, openssl_binary='openssl') now = backend.get_now() - assert now.tzinfo is None - assert now == datetime.datetime(2024, 2, 3, 4, 5, 6) + assert now.tzinfo is not None + assert now == datetime.datetime(2024, 2, 3, 4, 5, 6, tzinfo=UTC) @pytest.mark.parametrize("timezone, input, expected", TEST_PARSE_ACME_TIMESTAMP)