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

@@ -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)})"
)