Remove Python 2 specific code (#877)

* Get rid of Python 2 special handling.

* Get rid of more Python 2 specific handling.

* Stop using six.

* ipaddress is part of the standard library since Python 3.

* Add changelog.

* Fix import.

* Remove unneeded imports.
This commit is contained in:
Felix Fontein
2025-05-01 16:21:13 +02:00
committed by GitHub
parent 641e63b08c
commit 65872e884f
29 changed files with 269 additions and 565 deletions

View File

@@ -0,0 +1,4 @@
minor_changes:
- "Python code modernization: use f-strings instead of ``%`` and ``str.format()`` (https://github.com/ansible-collections/community.crypto/pull/875)."
- "Python code modernization: update ``__future__`` imports, remove Python 2 specific boilerplates (https://github.com/ansible-collections/community.crypto/pull/876)."
- "Python code modernization: remove Python 3 specific code (https://github.com/ansible-collections/community.crypto/pull/877)."

View File

@@ -19,7 +19,6 @@ notes:
to help us supporting it. Feedback that an ACME server not mentioned does work is also appreciated.
requirements:
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5
- ipaddress
options:
acme_version:
description:

View File

@@ -41,7 +41,6 @@ _value:
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.gnupg.cli import (
GPGError,
get_fingerprint_from_bytes,
@@ -52,7 +51,7 @@ from ansible_collections.community.crypto.plugins.plugin_utils.gnupg import (
def gpg_fingerprint(input):
if not isinstance(input, string_types):
if not isinstance(input, (str, bytes)):
raise AnsibleFilterError(
f"The input for the community.crypto.gpg_fingerprint filter must be a string; got {type(input)} instead"
)

View File

@@ -276,7 +276,6 @@ _value:
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
@@ -290,11 +289,11 @@ from ansible_collections.community.crypto.plugins.plugin_utils.filter_module imp
def openssl_csr_info_filter(data, name_encoding="ignore"):
"""Extract information from X.509 PEM certificate."""
if not isinstance(data, string_types):
if not isinstance(data, (str, bytes)):
raise AnsibleFilterError(
f"The community.crypto.openssl_csr_info input must be a text type, not {type(data)}"
)
if not isinstance(name_encoding, string_types):
if not isinstance(name_encoding, (str, bytes)):
raise AnsibleFilterError(
f"The name_encoding option must be of a text type, not {type(name_encoding)}"
)

View File

@@ -148,7 +148,6 @@ _value:
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
@@ -165,11 +164,11 @@ def openssl_privatekey_info_filter(
data, passphrase=None, return_private_key_data=False
):
"""Extract information from X.509 PEM certificate."""
if not isinstance(data, string_types):
if not isinstance(data, (str, bytes)):
raise AnsibleFilterError(
f"The community.crypto.openssl_privatekey_info input must be a text type, not {type(data)}"
)
if passphrase is not None and not isinstance(passphrase, string_types):
if passphrase is not None and not isinstance(passphrase, (str, bytes)):
raise AnsibleFilterError(
f"The passphrase option must be a text type, not {type(passphrase)}"
)

View File

@@ -125,7 +125,6 @@ _value:
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
@@ -140,7 +139,7 @@ from ansible_collections.community.crypto.plugins.plugin_utils.filter_module imp
def openssl_publickey_info_filter(data):
"""Extract information from OpenSSL PEM public key."""
if not isinstance(data, string_types):
if not isinstance(data, (str, bytes)):
raise AnsibleFilterError(
f"The community.crypto.openssl_publickey_info input must be a text type, not {type(data)}"
)

View File

@@ -41,14 +41,13 @@ _value:
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.serial import (
parse_serial,
)
def parse_serial_filter(input):
if not isinstance(input, string_types):
if not isinstance(input, (str, bytes)):
raise AnsibleFilterError(
f"The input for the community.crypto.parse_serial filter must be a string; got {type(input)} instead"
)

View File

@@ -40,7 +40,6 @@ _value:
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
split_pem_list,
)
@@ -48,7 +47,7 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import
def split_pem_filter(data):
"""Split PEM file."""
if not isinstance(data, string_types):
if not isinstance(data, (str, bytes)):
raise AnsibleFilterError(
f"The community.crypto.split_pem input must be a text type, not {type(data)}"
)

View File

@@ -41,12 +41,11 @@ _value:
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import integer_types
from ansible_collections.community.crypto.plugins.module_utils.serial import to_serial
def to_serial_filter(input):
if not isinstance(input, integer_types):
if not isinstance(input, int):
raise AnsibleFilterError(
f"The input for the community.crypto.to_serial filter must be an integer; got {type(input)} instead"
)

View File

@@ -310,7 +310,6 @@ _value:
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
@@ -324,11 +323,11 @@ from ansible_collections.community.crypto.plugins.plugin_utils.filter_module imp
def x509_certificate_info_filter(data, name_encoding="ignore"):
"""Extract information from X.509 PEM certificate."""
if not isinstance(data, string_types):
if not isinstance(data, (str, bytes)):
raise AnsibleFilterError(
f"The community.crypto.x509_certificate_info input must be a text type, not {type(data)}"
)
if not isinstance(name_encoding, string_types):
if not isinstance(name_encoding, (str, bytes)):
raise AnsibleFilterError(
f"The name_encoding option must be of a text type, not {type(name_encoding)}"
)

View File

@@ -158,7 +158,6 @@ import binascii
from ansible.errors import AnsibleFilterError
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
@@ -175,11 +174,11 @@ from ansible_collections.community.crypto.plugins.plugin_utils.filter_module imp
def x509_crl_info_filter(data, name_encoding="ignore", list_revoked_certificates=True):
"""Extract information from X.509 PEM certificate."""
if not isinstance(data, string_types):
if not isinstance(data, (str, bytes)):
raise AnsibleFilterError(
f"The community.crypto.x509_crl_info input must be a text type, not {type(data)}"
)
if not isinstance(name_encoding, string_types):
if not isinstance(name_encoding, (str, bytes)):
raise AnsibleFilterError(
f"The name_encoding option must be of a text type, not {type(name_encoding)}"
)

View File

@@ -10,11 +10,9 @@ import datetime
import json
import locale
import time
import traceback
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes
from ansible.module_utils.six import PY3
from ansible.module_utils.urls import fetch_url
from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import (
CRYPTOGRAPHY_ERROR,
@@ -43,16 +41,6 @@ from ansible_collections.community.crypto.plugins.module_utils.argspec import (
)
try:
import ipaddress # noqa: F401, pylint: disable=unused-import
except ImportError:
HAS_IPADDRESS = False
IPADDRESS_IMPORT_ERROR = traceback.format_exc()
else:
HAS_IPADDRESS = True
IPADDRESS_IMPORT_ERROR = None
# -1 usually means connection problems
RETRY_STATUS_CODES = (-1, 408, 429, 503)
@@ -345,7 +333,7 @@ class ACMEClient:
try:
# In Python 2, reading from a closed response yields a TypeError.
# In Python 3, read() simply returns ''
if PY3 and resp.closed:
if resp.closed:
raise TypeError
content = resp.read()
except (AttributeError, TypeError):
@@ -440,7 +428,7 @@ class ACMEClient:
try:
# In Python 2, reading from a closed response yields a TypeError.
# In Python 3, read() simply returns ''
if PY3 and resp.closed:
if resp.closed:
raise TypeError
content = resp.read()
except (AttributeError, TypeError):
@@ -557,11 +545,6 @@ def create_default_argspec(
def create_backend(module, needs_acme_v2=True):
if not HAS_IPADDRESS:
module.fail_json(
msg=missing_required_lib("ipaddress"), exception=IPADDRESS_IMPORT_ERROR
)
backend = module.params["select_crypto_backend"]
# Backend autodetect

View File

@@ -8,6 +8,7 @@ from __future__ import annotations
import base64
import binascii
import datetime
import ipaddress
import os
import re
import tempfile
@@ -33,12 +34,6 @@ from ansible_collections.community.crypto.plugins.module_utils.time import (
)
try:
import ipaddress
except ImportError:
pass
_OPENSSL_ENVIRONMENT_UPDATE = dict(LANG="C", LC_ALL="C", LC_MESSAGES="C", LC_CTYPE="C")

View File

@@ -74,7 +74,6 @@ def _parse_acme_timestamp(timestamp_str, with_timezone):
"%Y-%m-%dT%H:%M:%S%z",
"%Y-%m-%dT%H:%M:%S.%f%z",
):
# Note that %z will not work with Python 2... https://stackoverflow.com/a/27829491
try:
result = datetime.datetime.strptime(timestamp_str, format)
except ValueError:

View File

@@ -7,6 +7,7 @@ from __future__ import annotations
import base64
import hashlib
import ipaddress
import json
import re
import time
@@ -22,12 +23,6 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import
)
try:
import ipaddress
except ImportError:
pass
def create_key_authorization(client, token):
"""
Returns the key authorization for the given token

View File

@@ -5,9 +5,9 @@
from __future__ import annotations
from http.client import responses as http_responses
from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils.six import PY3, binary_type
from ansible.module_utils.six.moves.http_client import responses as http_responses
def format_http_status(status_code):
@@ -67,7 +67,7 @@ class ACMEProtocolException(ModuleFailException):
try:
# In Python 2, reading from a closed response yields a TypeError.
# In Python 3, read() simply returns ''
if PY3 and response.closed:
if response.closed:
raise TypeError
content = response.read()
except (AttributeError, TypeError):
@@ -75,7 +75,7 @@ class ACMEProtocolException(ModuleFailException):
# Make sure that content_json is None or a dictionary
if content_json is not None and not isinstance(content_json, dict):
if content is None and isinstance(content_json, binary_type):
if content is None and isinstance(content_json, bytes):
content = content_json
content_json = None

View File

@@ -10,9 +10,9 @@ import datetime
import re
import textwrap
import traceback
from urllib.parse import unquote
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six.moves.urllib.parse import unquote
from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
ModuleFailException,
)

View File

@@ -6,16 +6,16 @@ from __future__ import annotations
import base64
import binascii
import ipaddress
import re
import sys
import traceback
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.module_utils.six.moves.urllib.parse import (
from urllib.parse import (
ParseResult,
urlparse,
urlunparse,
)
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible_collections.community.crypto.plugins.module_utils.version import (
LooseVersion,
)
@@ -24,8 +24,6 @@ from ._asn1 import serialize_asn1_string_as_der
try:
import ipaddress
import cryptography
from cryptography import x509
from cryptography.exceptions import InvalidSignature
@@ -286,12 +284,8 @@ DN_COMPONENT_START_RE = re.compile(b"^ *([a-zA-z0-9.]+) *= *")
DN_HEX_LETTER = b"0123456789abcdef"
if sys.version_info[0] < 3:
_int_to_byte = chr
else:
def _int_to_byte(value):
return bytes((value,))
def _int_to_byte(value):
return bytes((value,))
def _parse_dn_component(name, sep=b",", decode_remainder=True):

View File

@@ -4,8 +4,6 @@
from __future__ import annotations
import sys
def binary_exp_mod(f, e, m):
"""Computes f^e mod m in O(log e) multiplications modulo m."""
@@ -99,82 +97,36 @@ def quick_is_not_prime(n):
return False
python_version = (sys.version_info[0], sys.version_info[1])
if python_version >= (2, 7) or python_version >= (3, 1):
# Ansible still supports Python 2.6 on remote nodes
def count_bytes(no):
"""
Given an integer, compute the number of bytes necessary to store its absolute value.
"""
no = abs(no)
if no == 0:
return 0
return (no.bit_length() + 7) // 8
def count_bits(no):
"""
Given an integer, compute the number of bits necessary to store its absolute value.
"""
no = abs(no)
if no == 0:
return 0
return no.bit_length()
else:
# Slow, but works
def count_bytes(no):
"""
Given an integer, compute the number of bytes necessary to store its absolute value.
"""
no = abs(no)
count = 0
while no > 0:
no >>= 8
count += 1
return count
def count_bits(no):
"""
Given an integer, compute the number of bits necessary to store its absolute value.
"""
no = abs(no)
count = 0
while no > 0:
no >>= 1
count += 1
return count
def count_bytes(no):
"""
Given an integer, compute the number of bytes necessary to store its absolute value.
"""
no = abs(no)
if no == 0:
return 0
return (no.bit_length() + 7) // 8
if sys.version_info[0] >= 3:
# Python 3 (and newer)
def _convert_int_to_bytes(count, no):
return no.to_bytes(count, byteorder="big")
def count_bits(no):
"""
Given an integer, compute the number of bits necessary to store its absolute value.
"""
no = abs(no)
if no == 0:
return 0
return no.bit_length()
def _convert_bytes_to_int(data):
return int.from_bytes(data, byteorder="big", signed=False)
def _to_hex(no):
return hex(no)[2:]
def _convert_int_to_bytes(count, no):
return no.to_bytes(count, byteorder="big")
else:
# Python 2
def _convert_int_to_bytes(count, n):
if n == 0 and count == 0:
return ""
h = f"{n:x}"
if len(h) > 2 * count:
raise Exception(f"Number {n} needs more than {count} bytes!")
return ("0" * (2 * count - len(h)) + h).decode("hex")
def _convert_bytes_to_int(data):
v = 0
for x in data:
v = (v << 8) | ord(x)
return v
def _convert_bytes_to_int(data):
return int.from_bytes(data, byteorder="big", signed=False)
def _to_hex(no):
return f"{no:x}"
def _to_hex(no):
return f"{no:x}"
def convert_int_to_bytes(no, count=None):

View File

@@ -14,11 +14,11 @@ import json
import os
import re
import traceback
from urllib.error import HTTPError
from urllib.parse import urlencode
from ansible.module_utils.basic import missing_required_lib
from ansible.module_utils.common.text.converters import to_native, to_text
from ansible.module_utils.six.moves.urllib.error import HTTPError
from ansible.module_utils.six.moves.urllib.parse import urlencode
from ansible.module_utils.urls import Request

View File

@@ -8,7 +8,6 @@ import abc
import binascii
import datetime as _datetime
import os
import sys
from base64 import b64encode
from datetime import datetime
from hashlib import sha256
@@ -68,13 +67,8 @@ _ECDSA_CURVE_IDENTIFIERS_LOOKUP = {
b"nistp521": "ecdsa-nistp521",
}
_USE_TIMEZONE = sys.version_info >= (3, 6)
_ALWAYS = _add_or_remove_timezone(datetime(1970, 1, 1), with_timezone=_USE_TIMEZONE)
_FOREVER = (
datetime(9999, 12, 31, 23, 59, 59, 999999, _UTC) if _USE_TIMEZONE else datetime.max
)
_ALWAYS = _add_or_remove_timezone(datetime(1970, 1, 1), with_timezone=True)
_FOREVER = datetime(9999, 12, 31, 23, 59, 59, 999999, _UTC)
_CRITICAL_OPTIONS = (
"force-command",
@@ -99,9 +93,6 @@ _EXTENSIONS = (
"permit-user-rc",
)
if six.PY3:
long = int
class OpensshCertificateTimeParameters:
def __init__(self, valid_from, valid_to):
@@ -172,13 +163,13 @@ class OpensshCertificateTimeParameters:
result = OpensshCertificateTimeParameters._time_string_to_datetime(
time_string_or_timestamp.strip()
)
elif isinstance(time_string_or_timestamp, (long, int)):
elif isinstance(time_string_or_timestamp, int):
result = OpensshCertificateTimeParameters._timestamp_to_datetime(
time_string_or_timestamp
)
else:
raise ValueError(
f"Value must be of type (str, unicode, int, long) not {type(time_string_or_timestamp)}"
f"Value must be of type (str, unicode, int) not {type(time_string_or_timestamp)}"
)
except ValueError:
raise
@@ -192,12 +183,7 @@ class OpensshCertificateTimeParameters:
result = _FOREVER
else:
try:
if _USE_TIMEZONE:
result = datetime.fromtimestamp(
timestamp, tz=_datetime.timezone.utc
)
else:
result = datetime.utcfromtimestamp(timestamp)
result = datetime.fromtimestamp(timestamp, tz=_datetime.timezone.utc)
except OverflowError:
raise ValueError
return result
@@ -210,15 +196,13 @@ class OpensshCertificateTimeParameters:
elif time_string == "forever":
result = _FOREVER
elif is_relative_time_string(time_string):
result = convert_relative_to_datetime(
time_string, with_timezone=_USE_TIMEZONE
)
result = convert_relative_to_datetime(time_string, with_timezone=True)
else:
for time_format in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
try:
result = _add_or_remove_timezone(
datetime.strptime(time_string, time_format),
with_timezone=_USE_TIMEZONE,
with_timezone=True,
)
except ValueError:
pass

View File

@@ -10,8 +10,6 @@ import re
from contextlib import contextmanager
from struct import Struct
from ansible.module_utils.six import PY3
# Protocol References
# -------------------
@@ -25,9 +23,6 @@ from ansible.module_utils.six import PY3
# https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/serialization/ssh.py
# https://github.com/paramiko/paramiko/blob/master/paramiko/message.py
if PY3:
long = int
# 0 (False) or 1 (True) encoded as a single byte
_BOOLEAN = Struct(b"?")
# Unsigned 8-bit integer in network-byte-order
@@ -93,7 +88,7 @@ class OpensshParser:
if not isinstance(data, (bytes, bytearray)):
raise TypeError(f"Data must be bytes-like not {type(data)}")
self._data = memoryview(data) if PY3 else data
self._data = memoryview(data)
self._pos = 0
def boolean(self):
@@ -125,7 +120,7 @@ class OpensshParser:
value = self._data[self._pos : next_pos]
self._pos = next_pos
# Cast to bytes is required as a memoryview slice is itself a memoryview
return value if not PY3 else bytes(value)
return bytes(value)
def mpint(self):
return self._big_int(self.string(), "big", signed=True)
@@ -223,47 +218,7 @@ class OpensshParser:
f"Byte_order must be one of (big, little) not {byte_order}"
)
if PY3:
return int.from_bytes(raw_string, byte_order, signed=signed)
result = 0
byte_length = len(raw_string)
if byte_length > 0:
# Check sign-bit
msb = raw_string[0] if byte_order == "big" else raw_string[-1]
negative = bool(ord(msb) & 0x80)
# Match pad value for two's complement
pad = b"\xff" if signed and negative else b"\x00"
# The definition of ``mpint`` enforces that unnecessary bytes are not encoded so they are added back
pad_length = 4 - byte_length % 4
if pad_length < 4:
raw_string = (
pad * pad_length + raw_string
if byte_order == "big"
else raw_string + pad * pad_length
)
byte_length += pad_length
# Accumulate arbitrary precision integer bytes in the appropriate order
if byte_order == "big":
for i in range(0, byte_length, cls.UINT32_OFFSET):
left_shift = result << cls.UINT32_OFFSET * 8
result = (
left_shift
+ _UINT32.unpack(raw_string[i : i + cls.UINT32_OFFSET])[0]
)
else:
for i in range(byte_length, 0, -cls.UINT32_OFFSET):
left_shift = result << cls.UINT32_OFFSET * 8
result = (
left_shift
+ _UINT32_LE.unpack(raw_string[i - cls.UINT32_OFFSET : i])[0]
)
# Adjust for two's complement
if signed and negative:
result -= 1 << (8 * byte_length)
return result
return int.from_bytes(raw_string, byte_order, signed=signed)
class _OpensshWriter:
@@ -307,8 +262,8 @@ class _OpensshWriter:
return self
def uint64(self, value):
if not isinstance(value, (long, int)):
raise TypeError(f"Value must be of type (long, int) not {type(value)}")
if not isinstance(value, int):
raise TypeError(f"Value must be of type int not {type(value)}")
if value < 0 or value > _UINT64_MAX:
raise ValueError(
f"Value must be a positive integer less than {_UINT64_MAX}"
@@ -327,8 +282,8 @@ class _OpensshWriter:
return self
def mpint(self, value):
if not isinstance(value, (int, long)):
raise TypeError(f"Value must be of type (long, int) not {type(value)}")
if not isinstance(value, int):
raise TypeError(f"Value must be of type int not {type(value)}")
self.string(self._int_to_mpint(value))
@@ -373,42 +328,12 @@ class _OpensshWriter:
@staticmethod
def _int_to_mpint(num):
if PY3:
byte_length = (num.bit_length() + 7) // 8
try:
result = num.to_bytes(byte_length, "big", signed=True)
# Handles values which require \x00 or \xFF to pad sign-bit
except OverflowError:
result = num.to_bytes(byte_length + 1, "big", signed=True)
else:
result = bytes()
# 0 and -1 are treated as special cases since they are used as sentinels for all other values
if num == 0:
result += b"\x00"
elif num == -1:
result += b"\xff"
elif num > 0:
while num >> 32:
result = _UINT32.pack(num & _UINT32_MAX) + result
num = num >> 32
# Pack last 4 bytes individually to discard insignificant bytes
while num:
result = _UBYTE.pack(num & _UBYTE_MAX) + result
num = num >> 8
# Zero pad final byte if most-significant bit is 1 as per mpint definition
if ord(result[0]) & 0x80:
result = b"\x00" + result
else:
while (num >> 32) < -1:
result = _UINT32.pack(num & _UINT32_MAX) + result
num = num >> 32
while num < -1:
result = _UBYTE.pack(num & _UBYTE_MAX) + result
num = num >> 8
if not ord(result[0]) & 0x80:
result = b"\xff" + result
return result
byte_length = (num.bit_length() + 7) // 8
try:
return num.to_bytes(byte_length, "big", signed=True)
# Handles values which require \x00 or \xFF to pad sign-bit
except OverflowError:
return num.to_bytes(byte_length + 1, "big", signed=True)
def bytes(self):
return bytes(self._buff)

View File

@@ -6,7 +6,6 @@ from __future__ import annotations
import datetime
import re
import sys
from ansible.module_utils.common.text.converters import to_native
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
@@ -68,29 +67,11 @@ def add_or_remove_timezone(timestamp, with_timezone):
)
if sys.version_info < (3, 3):
def get_epoch_seconds(timestamp):
epoch = datetime.datetime(
1970, 1, 1, tzinfo=UTC if timestamp.tzinfo is not None else None
)
delta = timestamp - epoch
try:
return delta.total_seconds()
except AttributeError:
# Python 2.6 and earlier: total_seconds() does not yet exist, so we use the formula from
# https://docs.python.org/2/library/datetime.html#datetime.timedelta.total_seconds
return (
delta.microseconds + (delta.seconds + delta.days * 24 * 3600) * 10**6
) / 10**6
else:
def get_epoch_seconds(timestamp):
if timestamp.tzinfo is None:
# timestamp.timestamp() is offset by the local timezone if timestamp has no timezone
timestamp = ensure_utc_timezone(timestamp)
return timestamp.timestamp()
def get_epoch_seconds(timestamp):
if timestamp.tzinfo is None:
# timestamp.timestamp() is offset by the local timezone if timestamp has no timezone
timestamp = ensure_utc_timezone(timestamp)
return timestamp.timestamp()
def from_epoch_seconds(timestamp, with_timezone):

View File

@@ -149,7 +149,7 @@ regular_certificate:
import base64
import datetime
import sys
import ipaddress
import traceback
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
@@ -173,8 +173,6 @@ from ansible_collections.community.crypto.plugins.module_utils.version import (
CRYPTOGRAPHY_IMP_ERR = None
try:
import ipaddress
import cryptography
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.asymmetric.ec
@@ -194,23 +192,12 @@ except ImportError:
# Convert byte string to ASN1 encoded octet string
if sys.version_info[0] >= 3:
def encode_octet_string(octet_string):
if len(octet_string) >= 128:
raise ModuleFailException(
"Cannot handle octet strings with more than 128 bytes"
)
return bytes([0x4, len(octet_string)]) + octet_string
else:
def encode_octet_string(octet_string):
if len(octet_string) >= 128:
raise ModuleFailException(
"Cannot handle octet strings with more than 128 bytes"
)
return b"\x04" + chr(len(octet_string)) + octet_string
def encode_octet_string(octet_string):
if len(octet_string) >= 128:
raise ModuleFailException(
"Cannot handle octet strings with more than 128 bytes"
)
return bytes([0x4, len(octet_string)]) + octet_string
def main():

View File

@@ -13,8 +13,6 @@ short_description: Get a certificate from a host:port
description:
- Makes a secure connection and returns information about the presented certificate.
- The module uses the cryptography Python library.
- Support SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) only with Python 2.7 and
newer.
extends_documentation_fragment:
- community.crypto.attributes
- community.crypto.attributes.idempotent_not_modify_state
@@ -122,7 +120,7 @@ options:
notes:
- When using ca_cert on OS X it has been reported that in some conditions the validate will always succeed.
requirements:
- "Python >= 2.7 when using O(proxy_host), and Python >= 3.10 when O(get_certificate_chain=true)"
- "Python >= 3.10 when O(get_certificate_chain=true)"
- "cryptography >= 1.6"
seealso:
@@ -270,11 +268,15 @@ import sys
import traceback
from os.path import isfile
from socket import create_connection, setdefaulttimeout, socket
from ssl import CERT_NONE, CERT_REQUIRED, DER_cert_to_PEM_cert, get_server_certificate
from ssl import (
CERT_NONE,
CERT_REQUIRED,
DER_cert_to_PEM_cert,
create_default_context,
)
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
CRYPTOGRAPHY_TIMEZONE,
cryptography_get_extensions_from_cert,
@@ -292,15 +294,6 @@ from ansible_collections.community.crypto.plugins.module_utils.version import (
MINIMAL_CRYPTOGRAPHY_VERSION = "1.6"
CREATE_DEFAULT_CONTEXT_IMP_ERR = None
try:
from ssl import create_default_context
except ImportError:
CREATE_DEFAULT_CONTEXT_IMP_ERR = traceback.format_exc()
HAS_CREATE_DEFAULT_CONTEXT = False
else:
HAS_CREATE_DEFAULT_CONTEXT = True
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
@@ -413,156 +406,124 @@ def main():
verified_chain = None
unverified_chain = None
if not HAS_CREATE_DEFAULT_CONTEXT:
# Python < 2.7.9
try:
if proxy_host:
module.fail_json(
msg="To use proxy_host, you must run the get_certificate module with Python 2.7 or newer.",
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR,
)
connect = f"CONNECT {host}:{port} HTTP/1.0\r\n\r\n"
sock = socket()
atexit.register(sock.close)
sock.connect((proxy_host, proxy_port))
sock.send(connect.encode())
sock.recv(8192)
else:
sock = create_connection((host, port))
atexit.register(sock.close)
if ca_cert:
ctx = create_default_context(cafile=ca_cert)
ctx.check_hostname = False
ctx.verify_mode = CERT_REQUIRED
else:
ctx = create_default_context()
ctx.check_hostname = False
ctx.verify_mode = CERT_NONE
if start_tls_server_type is not None:
send_starttls_packet(sock, start_tls_server_type)
if ciphers is not None:
module.fail_json(
msg="To use ciphers, you must run the get_certificate module with Python 2.7 or newer.",
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR,
)
ciphers_joined = ":".join(ciphers)
ctx.set_ciphers(ciphers_joined)
if tls_ctx_options is not None:
module.fail_json(
msg="To use tls_ctx_options, you must run the get_certificate module with Python 2.7 or newer.",
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR,
)
try:
# Note: get_server_certificate does not support SNI!
cert = get_server_certificate((host, port), ca_certs=ca_cert)
except Exception as e:
module.fail_json(msg=f"Failed to get cert from {host}:{port}, error: {e}")
else:
# Python >= 2.7.9
try:
if proxy_host:
connect = f"CONNECT {host}:{port} HTTP/1.0\r\n\r\n"
sock = socket()
atexit.register(sock.close)
sock.connect((proxy_host, proxy_port))
sock.send(connect.encode())
sock.recv(8192)
else:
sock = create_connection((host, port))
atexit.register(sock.close)
# Clear default ctx options
ctx.options = 0
if ca_cert:
ctx = create_default_context(cafile=ca_cert)
ctx.check_hostname = False
ctx.verify_mode = CERT_REQUIRED
else:
ctx = create_default_context()
ctx.check_hostname = False
ctx.verify_mode = CERT_NONE
if start_tls_server_type is not None:
send_starttls_packet(sock, start_tls_server_type)
if ciphers is not None:
ciphers_joined = ":".join(ciphers)
ctx.set_ciphers(ciphers_joined)
if tls_ctx_options is not None:
# Clear default ctx options
ctx.options = 0
# For each item in the tls_ctx_options list
for tls_ctx_option in tls_ctx_options:
# If the item is a string_type
if isinstance(tls_ctx_option, string_types):
# Convert tls_ctx_option to a native string
tls_ctx_option_str = to_native(tls_ctx_option)
# Get the tls_ctx_option_str attribute from ssl
tls_ctx_option_attr = getattr(ssl, tls_ctx_option_str, None)
# If tls_ctx_option_attr is an integer
if isinstance(tls_ctx_option_attr, int):
# Set tls_ctx_option_int to the attribute value
tls_ctx_option_int = tls_ctx_option_attr
# If tls_ctx_option_attr is not an integer
else:
module.fail_json(
msg=f"Failed to determine the numeric value for {tls_ctx_option_str}"
)
# If the item is an integer
elif isinstance(tls_ctx_option, int):
# Set tls_ctx_option_int to the item value
tls_ctx_option_int = tls_ctx_option
# If the item is not a string nor integer
# For each item in the tls_ctx_options list
for tls_ctx_option in tls_ctx_options:
# If the item is a string_type
if isinstance(tls_ctx_option, (str, bytes)):
# Convert tls_ctx_option to a native string
tls_ctx_option_str = to_native(tls_ctx_option)
# Get the tls_ctx_option_str attribute from ssl
tls_ctx_option_attr = getattr(ssl, tls_ctx_option_str, None)
# If tls_ctx_option_attr is an integer
if isinstance(tls_ctx_option_attr, int):
# Set tls_ctx_option_int to the attribute value
tls_ctx_option_int = tls_ctx_option_attr
# If tls_ctx_option_attr is not an integer
else:
module.fail_json(
msg=f"tls_ctx_options must be a string or integer, got {tls_ctx_option!r}"
msg=f"Failed to determine the numeric value for {tls_ctx_option_str}"
)
tls_ctx_option_int = (
0 # make pylint happy; this code is actually unreachable
)
try:
# Add the int value of the item to ctx options
ctx.options |= tls_ctx_option_int
except Exception:
module.fail_json(
msg=f"Failed to add {tls_ctx_option_str or tls_ctx_option_int} to CTX options"
)
tls_sock = ctx.wrap_socket(sock, server_hostname=server_name or host)
cert = tls_sock.getpeercert(True)
cert = DER_cert_to_PEM_cert(cert)
if get_certificate_chain:
if sys.version_info < (3, 13):
# The official way to access this has been added in https://github.com/python/cpython/pull/109113/files.
# We are basically doing the same for older Python versions. The internal API needed for this was added
# in https://github.com/python/cpython/commit/666991fc598bc312d72aff0078ecb553f0a968f1, which was first
# released in Python 3.10.0.
def _convert_chain(chain):
if not chain:
return []
return [c.public_bytes(ssl._ssl.ENCODING_DER) for c in chain]
ssl_obj = tls_sock._sslobj # This is of type ssl._ssl._SSLSocket
verified_der_chain = _convert_chain(ssl_obj.get_verified_chain())
unverified_der_chain = _convert_chain(
ssl_obj.get_unverified_chain()
)
# If the item is an integer
elif isinstance(tls_ctx_option, int):
# Set tls_ctx_option_int to the item value
tls_ctx_option_int = tls_ctx_option
# If the item is not a string nor integer
else:
# This works with Python 3.13+
# Unfortunately due to a bug (https://github.com/python/cpython/issues/118658) some early pre-releases of
# Python 3.13 do not return lists of byte strings, but lists of _ssl.Certificate objects. This is going to
# be fixed by https://github.com/python/cpython/pull/118669. For now we convert the certificates ourselves
# if they are not byte strings to work around this.
def _convert_chain(chain):
return [
(
c
if isinstance(c, bytes)
else c.public_bytes(ssl._ssl.ENCODING_DER)
)
for c in chain
]
verified_der_chain = _convert_chain(tls_sock.get_verified_chain())
unverified_der_chain = _convert_chain(
tls_sock.get_unverified_chain()
module.fail_json(
msg=f"tls_ctx_options must be a string or integer, got {tls_ctx_option!r}"
)
tls_ctx_option_int = (
0 # make pylint happy; this code is actually unreachable
)
verified_chain = [DER_cert_to_PEM_cert(c) for c in verified_der_chain]
unverified_chain = [
DER_cert_to_PEM_cert(c) for c in unverified_der_chain
]
try:
# Add the int value of the item to ctx options
ctx.options |= tls_ctx_option_int
except Exception:
module.fail_json(
msg=f"Failed to add {tls_ctx_option_str or tls_ctx_option_int} to CTX options"
)
except Exception as e:
if proxy_host:
module.fail_json(
msg=f"Failed to get cert via proxy {proxy_host}:{proxy_port} from {host}:{port}, error: {e}"
)
tls_sock = ctx.wrap_socket(sock, server_hostname=server_name or host)
cert = tls_sock.getpeercert(True)
cert = DER_cert_to_PEM_cert(cert)
if get_certificate_chain:
if sys.version_info < (3, 13):
# The official way to access this has been added in https://github.com/python/cpython/pull/109113/files.
# We are basically doing the same for older Python versions. The internal API needed for this was added
# in https://github.com/python/cpython/commit/666991fc598bc312d72aff0078ecb553f0a968f1, which was first
# released in Python 3.10.0.
def _convert_chain(chain):
if not chain:
return []
return [c.public_bytes(ssl._ssl.ENCODING_DER) for c in chain]
ssl_obj = tls_sock._sslobj # This is of type ssl._ssl._SSLSocket
verified_der_chain = _convert_chain(ssl_obj.get_verified_chain())
unverified_der_chain = _convert_chain(ssl_obj.get_unverified_chain())
else:
module.fail_json(
msg=f"Failed to get cert from {host}:{port}, error: {e}"
)
# This works with Python 3.13+
# Unfortunately due to a bug (https://github.com/python/cpython/issues/118658) some early pre-releases of
# Python 3.13 do not return lists of byte strings, but lists of _ssl.Certificate objects. This is going to
# be fixed by https://github.com/python/cpython/pull/118669. For now we convert the certificates ourselves
# if they are not byte strings to work around this.
def _convert_chain(chain):
return [
(
c
if isinstance(c, bytes)
else c.public_bytes(ssl._ssl.ENCODING_DER)
)
for c in chain
]
verified_der_chain = _convert_chain(tls_sock.get_verified_chain())
unverified_der_chain = _convert_chain(tls_sock.get_unverified_chain())
verified_chain = [DER_cert_to_PEM_cert(c) for c in verified_der_chain]
unverified_chain = [DER_cert_to_PEM_cert(c) for c in unverified_der_chain]
except Exception as e:
if proxy_host:
module.fail_json(
msg=f"Failed to get cert via proxy {proxy_host}:{proxy_port} from {host}:{port}, error: {e}"
)
else:
module.fail_json(msg=f"Failed to get cert from {host}:{port}, error: {e}")
result["cert"] = cert

View File

@@ -12,7 +12,7 @@ module: x509_certificate_info
short_description: Provide information of OpenSSL X.509 certificates
description:
- This module allows one to query information on OpenSSL certificates.
- It uses the cryptography python library to interact with OpenSSL.
- It uses the cryptography Python library to interact with OpenSSL.
- Note that this module was called C(openssl_certificate_info) when included directly in Ansible up to version 2.9. When
moved to the collection C(community.crypto), it was renamed to M(community.crypto.x509_certificate_info). From Ansible
2.10 on, it can still be used by the old short name (or by C(ansible.builtin.openssl_certificate_info)), which redirects
@@ -391,7 +391,6 @@ issuer_uri:
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native
from ansible.module_utils.six import string_types
from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
OpenSSLObjectError,
)
@@ -440,7 +439,7 @@ def main():
valid_at = module.params["valid_at"]
if valid_at:
for k, v in valid_at.items():
if not isinstance(v, string_types):
if not isinstance(v, (str, bytes)):
module.fail_json(
msg=f"The value for valid_at.{k} must be of type string (got {type(v)})"
)

View File

@@ -17,7 +17,6 @@ import copy
import traceback
from ansible.errors import AnsibleError
from ansible.module_utils import six
from ansible.module_utils.basic import SEQUENCETYPE, remove_values
from ansible.module_utils.common._collections_compat import Mapping
from ansible.module_utils.common.arg_spec import ArgumentSpecValidator
@@ -25,7 +24,6 @@ from ansible.module_utils.common.validation import (
safe_eval,
)
from ansible.module_utils.errors import UnsupportedError
from ansible.module_utils.six import string_types
from ansible.plugins.action import ActionBase
@@ -129,7 +127,7 @@ class AnsibleActionModule:
def warn(self, warning):
# Copied from ansible.module_utils.common.warnings:
if isinstance(warning, string_types):
if isinstance(warning, (str, bytes)):
self.__warnings.append(warning)
else:
raise TypeError(f"warn requires a string not a {type(warning)}")
@@ -141,7 +139,7 @@ class AnsibleActionModule:
)
# Copied from ansible.module_utils.common.warnings:
if isinstance(msg, string_types):
if isinstance(msg, (str, bytes)):
# For compatibility, we accept that neither version nor date is set,
# and treat that the same as if version would haven been set
if date is not None:
@@ -209,8 +207,7 @@ class AnsibleActionModule:
self._return_formatted(result)
@six.add_metaclass(abc.ABCMeta)
class ActionModuleBase(ActionBase):
class ActionModuleBase(ActionBase, metaclass=abc.ABCMeta):
@abc.abstractmethod
def setup_module(self):
"""Return pair (ArgumentSpec, kwargs)."""

View File

@@ -7,7 +7,6 @@ from __future__ import annotations
import base64
import datetime
import os
import sys
from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
CertificateInformation,
@@ -154,34 +153,25 @@ TEST_PARSE_ACME_TIMESTAMP = cartesian_product(
microsecond=333333,
),
),
(
"2024-01-01T00:11:22+0100",
dict(year=2023, month=12, day=31, hour=23, minute=11, second=22),
),
(
"2024-01-01T00:11:22.123+0100",
dict(
year=2023,
month=12,
day=31,
hour=23,
minute=11,
second=22,
microsecond=123000,
),
),
],
)
if sys.version_info >= (3, 5):
TEST_PARSE_ACME_TIMESTAMP.extend(
cartesian_product(
TIMEZONES,
[
(
"2024-01-01T00:11:22+0100",
dict(year=2023, month=12, day=31, hour=23, minute=11, second=22),
),
(
"2024-01-01T00:11:22.123+0100",
dict(
year=2023,
month=12,
day=31,
hour=23,
minute=11,
second=22,
microsecond=123000,
),
),
],
)
)
TEST_INTERPOLATE_TIMESTAMP = cartesian_product(
TIMEZONES,

View File

@@ -5,7 +5,6 @@
from __future__ import annotations
import datetime
import sys
import pytest
from ansible.module_utils.common.collections import is_sequence
@@ -47,6 +46,8 @@ def cartesian_product(list1, list2):
return result
ONE_HOUR_PLUS = datetime.timezone(datetime.timedelta(hours=1))
TEST_REMOVE_TIMEZONE = cartesian_product(
TIMEZONES,
[
@@ -58,6 +59,10 @@ TEST_REMOVE_TIMEZONE = cartesian_product(
datetime.datetime(2024, 1, 1, 0, 1, 2),
datetime.datetime(2024, 1, 1, 0, 1, 2),
),
(
datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=ONE_HOUR_PLUS),
datetime.datetime(2023, 12, 31, 23, 1, 2),
),
],
)
@@ -72,6 +77,10 @@ TEST_UTC_TIMEZONE = cartesian_product(
datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=UTC),
datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=UTC),
),
(
datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=ONE_HOUR_PLUS),
datetime.datetime(2023, 12, 31, 23, 1, 2, tzinfo=UTC),
),
],
)
@@ -109,6 +118,10 @@ TEST_EPOCH_TO_SECONDS = cartesian_product(
[
(datetime.datetime(1970, 1, 1, 0, 1, 2, 0), 62),
(datetime.datetime(1970, 1, 1, 0, 1, 2, 0, tzinfo=UTC), 62),
(
datetime.datetime(1970, 1, 1, 0, 1, 2, 0, tzinfo=ONE_HOUR_PLUS),
62 - 3600,
),
],
)
@@ -221,87 +234,42 @@ TEST_GET_RELATIVE_TIME_OPTION = cartesian_product(
datetime.datetime(2024, 1, 1, 0, 0, 0),
datetime.datetime(2024, 1, 2, 4, 5, 0, tzinfo=UTC),
),
(
"20240102040506+0100",
"foo",
"cryptography",
False,
datetime.datetime(2024, 1, 1, 0, 0, 0),
datetime.datetime(2024, 1, 2, 3, 5, 6),
),
(
"202401020405+0100",
"foo",
"cryptography",
False,
datetime.datetime(2024, 1, 1, 0, 0, 0),
datetime.datetime(2024, 1, 2, 3, 5, 0),
),
(
"20240102040506+0100",
"foo",
"cryptography",
True,
datetime.datetime(2024, 1, 1, 0, 0, 0),
datetime.datetime(2024, 1, 2, 3, 5, 6, tzinfo=UTC),
),
(
"202401020405+0100",
"foo",
"cryptography",
True,
datetime.datetime(2024, 1, 1, 0, 0, 0),
datetime.datetime(2024, 1, 2, 3, 5, 0, tzinfo=UTC),
),
],
)
if sys.version_info >= (3, 5):
ONE_HOUR_PLUS = datetime.timezone(datetime.timedelta(hours=1))
TEST_REMOVE_TIMEZONE.extend(
cartesian_product(
TIMEZONES,
[
(
datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=ONE_HOUR_PLUS),
datetime.datetime(2023, 12, 31, 23, 1, 2),
),
],
)
)
TEST_UTC_TIMEZONE.extend(
cartesian_product(
TIMEZONES,
[
(
datetime.datetime(2024, 1, 1, 0, 1, 2, tzinfo=ONE_HOUR_PLUS),
datetime.datetime(2023, 12, 31, 23, 1, 2, tzinfo=UTC),
),
],
)
)
TEST_EPOCH_TO_SECONDS.extend(
cartesian_product(
TIMEZONES,
[
(
datetime.datetime(1970, 1, 1, 0, 1, 2, 0, tzinfo=ONE_HOUR_PLUS),
62 - 3600,
),
],
)
)
TEST_GET_RELATIVE_TIME_OPTION.extend(
cartesian_product(
TIMEZONES,
[
(
"20240102040506+0100",
"foo",
"cryptography",
False,
datetime.datetime(2024, 1, 1, 0, 0, 0),
datetime.datetime(2024, 1, 2, 3, 5, 6),
),
(
"202401020405+0100",
"foo",
"cryptography",
False,
datetime.datetime(2024, 1, 1, 0, 0, 0),
datetime.datetime(2024, 1, 2, 3, 5, 0),
),
(
"20240102040506+0100",
"foo",
"cryptography",
True,
datetime.datetime(2024, 1, 1, 0, 0, 0),
datetime.datetime(2024, 1, 2, 3, 5, 6, tzinfo=UTC),
),
(
"202401020405+0100",
"foo",
"cryptography",
True,
datetime.datetime(2024, 1, 1, 0, 0, 0),
datetime.datetime(2024, 1, 2, 3, 5, 0, tzinfo=UTC),
),
],
)
)
@pytest.mark.parametrize("timezone, input, expected", TEST_REMOVE_TIMEZONE)
def test_remove_timezone(timezone, input, expected):
with freeze_time("2024-02-03 04:05:06", tz_offset=timezone):