Skip to content

Commit

Permalink
Explicitly use UTC timezone in ACME OpenSSL backend (#811)
Browse files Browse the repository at this point in the history
* Allow abstract backend class to handle both with and without timezone.

* Explicitly use UTC timezone in OpenSSL backend code.
  • Loading branch information
felixfontein authored Oct 27, 2024
1 parent feee571 commit 6731b38
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 62 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/811-openssl-timezone.yml
Original file line number Diff line number Diff line change
@@ -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)."
45 changes: 4 additions & 41 deletions plugins/module_utils/acme/backend_cryptography.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import base64
import binascii
import datetime
import os
import traceback

Expand All @@ -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 (
Expand All @@ -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,
Expand All @@ -64,12 +58,7 @@
)

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,
add_or_remove_timezone,
)

CRYPTOGRAPHY_MINIMAL_VERSION = '1.5'
Expand Down Expand Up @@ -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):
'''
Expand Down Expand Up @@ -419,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):
Expand Down
14 changes: 11 additions & 3 deletions plugins/module_utils/acme/backend_openssl_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down
21 changes: 13 additions & 8 deletions plugins/module_utils/acme/backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
get_now_datetime,
get_relative_time_option,
remove_timezone,
UTC,
)


Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
22 changes: 18 additions & 4 deletions tests/unit/plugins/module_utils/acme/test_backend_openssl_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,7 +33,7 @@
TEST_INTERPOLATE_TIMESTAMP,
)

from ..test_time import TIMEZONES
# from ..test_time import TIMEZONES


TEST_IPS = [
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 6731b38

Please sign in to comment.