Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicitly use UTC timezone in ACME OpenSSL backend #811

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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