diff --git a/plugins/doc_fragments/__init__.py b/plugins/doc_fragments/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/plugins/module_utils/__init__.py b/plugins/module_utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/plugins/module_utils/crypto/__init__.py b/plugins/module_utils/crypto/__init__.py new file mode 100644 index 00000000..b31011f9 --- /dev/null +++ b/plugins/module_utils/crypto/__init__.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +# +# (c) 2016, Yanis Guenane +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +# THIS FILE IS FOR COMPATIBILITY ONLY! YOU SHALL NOT IMPORT IT! +# +# This fill will be removed eventually, so if you're using it, +# please stop doing so. + +from .basic import ( + HAS_PYOPENSSL, + CRYPTOGRAPHY_HAS_X25519, + CRYPTOGRAPHY_HAS_X25519_FULL, + CRYPTOGRAPHY_HAS_X448, + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED448, + HAS_CRYPTOGRAPHY, + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from .cryptography_crl import ( + REVOCATION_REASON_MAP, + REVOCATION_REASON_MAP_INVERSE, + cryptography_decode_revoked_certificate, +) + +from .cryptography_support import ( + cryptography_get_extensions_from_cert, + cryptography_get_extensions_from_csr, + cryptography_name_to_oid, + cryptography_oid_to_name, + cryptography_get_name, + cryptography_decode_name, + cryptography_parse_key_usage_params, + cryptography_get_basic_constraints, + cryptography_key_needs_digest_for_signing, + cryptography_compare_public_keys, +) + +from .identify import ( + identify_private_key_format, +) + +from .math import ( + binary_exp_mod, + simple_gcd, + quick_is_not_prime, + count_bits, +) + +from ._obj2txt import obj2txt as _obj2txt + +from ._objects_data import OID_MAP as _OID_MAP + +from ._objects import OID_LOOKUP as _OID_LOOKUP +from ._objects import NORMALIZE_NAMES as _NORMALIZE_NAMES +from ._objects import NORMALIZE_NAMES_SHORT as _NORMALIZE_NAMES_SHORT + +from .pyopenssl_support import ( + pyopenssl_normalize_name, + pyopenssl_get_extensions_from_cert, + pyopenssl_get_extensions_from_csr, +) + +from .support import ( + get_fingerprint_of_bytes, + get_fingerprint, + load_privatekey, + load_certificate, + load_certificate_request, + parse_name_field, + convert_relative_to_datetime, + get_relative_time_option, + select_message_digest, + OpenSSLObject, +) + +from ..io import ( + load_file_if_exists, + write_file, +) diff --git a/plugins/module_utils/crypto/_obj2txt.py b/plugins/module_utils/crypto/_obj2txt.py new file mode 100644 index 00000000..f84774cb --- /dev/null +++ b/plugins/module_utils/crypto/_obj2txt.py @@ -0,0 +1,43 @@ +# This excerpt is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file at +# https://github.com/pyca/cryptography/blob/master/LICENSE for complete details. +# +# Adapted from cryptography's hazmat/backends/openssl/decode_asn1.py +# +# Copyright (c) 2015, 2016 Paul Kehrer (@reaperhulk) +# Copyright (c) 2017 Fraser Tweedale (@frasertweedale) + +# Relevant commits from cryptography project (https://github.com/pyca/cryptography): +# pyca/cryptography@719d536dd691e84e208534798f2eb4f82aaa2e07 +# pyca/cryptography@5ab6d6a5c05572bd1c75f05baf264a2d0001894a +# pyca/cryptography@2e776e20eb60378e0af9b7439000d0e80da7c7e3 +# pyca/cryptography@fb309ed24647d1be9e319b61b1f2aa8ebb87b90b +# pyca/cryptography@2917e460993c475c72d7146c50dc3bbc2414280d +# pyca/cryptography@3057f91ea9a05fb593825006d87a391286a4d828 +# pyca/cryptography@d607dd7e5bc5c08854ec0c9baff70ba4a35be36f + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +def obj2txt(openssl_lib, openssl_ffi, obj): + # Set to 80 on the recommendation of + # https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values + # + # But OIDs longer than this occur in real life (e.g. Active + # Directory makes some very long OIDs). So we need to detect + # and properly handle the case where the default buffer is not + # big enough. + # + buf_len = 80 + buf = openssl_ffi.new("char[]", buf_len) + + # 'res' is the number of bytes that *would* be written if the + # buffer is large enough. If 'res' > buf_len - 1, we need to + # alloc a big-enough buffer and go again. + res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1) + if res > buf_len - 1: # account for terminating null byte + buf_len = res + 1 + buf = openssl_ffi.new("char[]", buf_len) + res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1) + return openssl_ffi.buffer(buf, res)[:].decode() diff --git a/plugins/module_utils/crypto/_objects.py b/plugins/module_utils/crypto/_objects.py new file mode 100644 index 00000000..3785c1b0 --- /dev/null +++ b/plugins/module_utils/crypto/_objects.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# +# (c) 2019, Felix Fontein +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from ._objects_data import OID_MAP + +OID_LOOKUP = dict() +NORMALIZE_NAMES = dict() +NORMALIZE_NAMES_SHORT = dict() + +for dotted, names in OID_MAP.items(): + for name in names: + if name in NORMALIZE_NAMES and OID_LOOKUP[name] != dotted: + raise AssertionError( + 'Name collision during setup: "{0}" for OIDs {1} and {2}' + .format(name, dotted, OID_LOOKUP[name]) + ) + NORMALIZE_NAMES[name] = names[0] + NORMALIZE_NAMES_SHORT[name] = names[-1] + OID_LOOKUP[name] = dotted +for alias, original in [('userID', 'userId')]: + if alias in NORMALIZE_NAMES: + raise AssertionError( + 'Name collision during adding aliases: "{0}" (alias for "{1}") is already mapped to OID {2}' + .format(alias, original, OID_LOOKUP[alias]) + ) + NORMALIZE_NAMES[alias] = original + NORMALIZE_NAMES_SHORT[alias] = NORMALIZE_NAMES_SHORT[original] + OID_LOOKUP[alias] = OID_LOOKUP[original] diff --git a/plugins/module_utils/crypto.py b/plugins/module_utils/crypto/_objects_data.py similarity index 60% rename from plugins/module_utils/crypto.py rename to plugins/module_utils/crypto/_objects_data.py index be331774..c3b5446a 100644 --- a/plugins/module_utils/crypto.py +++ b/plugins/module_utils/crypto/_objects_data.py @@ -1,529 +1,17 @@ -# -*- coding: utf-8 -*- -# -# (c) 2016, Yanis Guenane -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . -# -# ---------------------------------------------------------------------- -# A clearly marked portion of this file is licensed under the BSD license -# Copyright (c) 2015, 2016 Paul Kehrer (@reaperhulk) -# Copyright (c) 2017 Fraser Tweedale (@frasertweedale) -# For more details, search for the function _obj2txt(). -# --------------------------------------------------------------------- -# A clearly marked portion of this file is extracted from a project that -# is licensed under the Apache License 2.0 +# This has been extracted from the OpenSSL project's objects.txt: +# https://github.com/openssl/openssl/blob/9537fe5757bb07761fa275d779bbd40bcf5530e4/crypto/objects/objects.txt +# Extracted with https://gist.github.com/felixfontein/376748017ad65ead093d56a45a5bf376 + +# In case the following data structure has any copyrightable content, note that it is licensed as follows: # Copyright (c) the OpenSSL contributors -# For more details, search for the function _OID_MAP. +# Licensed under the Apache License 2.0 +# https://github.com/openssl/openssl/blob/master/LICENSE from __future__ import absolute_import, division, print_function __metaclass__ = type -import abc -import base64 -import binascii -import datetime -import errno -import hashlib -import os -import re -import sys -import tempfile - -from distutils.version import LooseVersion - -from ansible.module_utils import six -from ansible.module_utils._text import to_native, to_bytes, to_text - -try: - import OpenSSL - from OpenSSL import crypto -except ImportError: - # An error will be raised in the calling class to let the end - # user know that OpenSSL couldn't be found. - pass - -try: - import cryptography - from cryptography import x509 - from cryptography.hazmat.backends import default_backend as cryptography_backend - from cryptography.hazmat.primitives.serialization import load_pem_private_key - from cryptography.hazmat.primitives import hashes - from cryptography.hazmat.primitives import serialization - import ipaddress - - # Older versions of cryptography (< 2.1) do not have __hash__ functions for - # general name objects (DNSName, IPAddress, ...), while providing overloaded - # equality and string representation operations. This makes it impossible to - # use them in hash-based data structures such as set or dict. Since we are - # actually doing that in x509_certificate, and potentially in other code, - # we need to monkey-patch __hash__ for these classes to make sure our code - # works fine. - if LooseVersion(cryptography.__version__) < LooseVersion('2.1'): - # A very simply hash function which relies on the representation - # of an object to be implemented. This is the case since at least - # cryptography 1.0, see - # https://github.com/pyca/cryptography/commit/7a9abce4bff36c05d26d8d2680303a6f64a0e84f - def simple_hash(self): - return hash(repr(self)) - - # The hash functions for the following types were added for cryptography 2.1: - # https://github.com/pyca/cryptography/commit/fbfc36da2a4769045f2373b004ddf0aff906cf38 - x509.DNSName.__hash__ = simple_hash - x509.DirectoryName.__hash__ = simple_hash - x509.GeneralName.__hash__ = simple_hash - x509.IPAddress.__hash__ = simple_hash - x509.OtherName.__hash__ = simple_hash - x509.RegisteredID.__hash__ = simple_hash - - if LooseVersion(cryptography.__version__) < LooseVersion('1.2'): - # The hash functions for the following types were added for cryptography 1.2: - # https://github.com/pyca/cryptography/commit/b642deed88a8696e5f01ce6855ccf89985fc35d0 - # https://github.com/pyca/cryptography/commit/d1b5681f6db2bde7a14625538bd7907b08dfb486 - x509.RFC822Name.__hash__ = simple_hash - x509.UniformResourceIdentifier.__hash__ = simple_hash - - # Test whether we have support for X25519, X448, Ed25519 and/or Ed448 - try: - import cryptography.hazmat.primitives.asymmetric.x25519 - CRYPTOGRAPHY_HAS_X25519 = True - try: - cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.private_bytes - CRYPTOGRAPHY_HAS_X25519_FULL = True - except AttributeError: - CRYPTOGRAPHY_HAS_X25519_FULL = False - except ImportError: - CRYPTOGRAPHY_HAS_X25519 = False - CRYPTOGRAPHY_HAS_X25519_FULL = False - try: - import cryptography.hazmat.primitives.asymmetric.x448 - CRYPTOGRAPHY_HAS_X448 = True - except ImportError: - CRYPTOGRAPHY_HAS_X448 = False - try: - import cryptography.hazmat.primitives.asymmetric.ed25519 - CRYPTOGRAPHY_HAS_ED25519 = True - except ImportError: - CRYPTOGRAPHY_HAS_ED25519 = False - try: - import cryptography.hazmat.primitives.asymmetric.ed448 - CRYPTOGRAPHY_HAS_ED448 = True - except ImportError: - CRYPTOGRAPHY_HAS_ED448 = False - - HAS_CRYPTOGRAPHY = True -except ImportError: - # Error handled in the calling module. - CRYPTOGRAPHY_HAS_X25519 = False - CRYPTOGRAPHY_HAS_X25519_FULL = False - CRYPTOGRAPHY_HAS_X448 = False - CRYPTOGRAPHY_HAS_ED25519 = False - CRYPTOGRAPHY_HAS_ED448 = False - HAS_CRYPTOGRAPHY = False - - -class OpenSSLObjectError(Exception): - pass - - -class OpenSSLBadPassphraseError(OpenSSLObjectError): - pass - - -def get_fingerprint_of_bytes(source): - """Generate the fingerprint of the given bytes.""" - - fingerprint = {} - - try: - algorithms = hashlib.algorithms - except AttributeError: - try: - algorithms = hashlib.algorithms_guaranteed - except AttributeError: - return None - - for algo in algorithms: - f = getattr(hashlib, algo) - try: - h = f(source) - except ValueError: - # This can happen for hash algorithms not supported in FIPS mode - # (https://github.com/ansible/ansible/issues/67213) - continue - try: - # Certain hash functions have a hexdigest() which expects a length parameter - pubkey_digest = h.hexdigest() - except TypeError: - pubkey_digest = h.hexdigest(32) - fingerprint[algo] = ':'.join(pubkey_digest[i:i + 2] for i in range(0, len(pubkey_digest), 2)) - - return fingerprint - - -def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl'): - """Generate the fingerprint of the public key. """ - - privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend) - - if backend == 'pyopenssl': - try: - publickey = crypto.dump_publickey(crypto.FILETYPE_ASN1, privatekey) - except AttributeError: - # If PyOpenSSL < 16.0 crypto.dump_publickey() will fail. - try: - bio = crypto._new_mem_buf() - rc = crypto._lib.i2d_PUBKEY_bio(bio, privatekey._pkey) - if rc != 1: - crypto._raise_current_error() - publickey = crypto._bio_to_string(bio) - except AttributeError: - # By doing this we prevent the code from raising an error - # yet we return no value in the fingerprint hash. - return None - elif backend == 'cryptography': - publickey = privatekey.public_key().public_bytes( - serialization.Encoding.DER, - serialization.PublicFormat.SubjectPublicKeyInfo - ) - - return get_fingerprint_of_bytes(publickey) - - -def load_file_if_exists(path, module=None, ignore_errors=False): - try: - with open(path, 'rb') as f: - return f.read() - except EnvironmentError as exc: - if exc.errno == errno.ENOENT: - return None - if ignore_errors: - return None - if module is None: - raise - module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) - except Exception as exc: - if ignore_errors: - return None - if module is None: - raise - module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) - - -def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='pyopenssl'): - """Load the specified OpenSSL private key. - - The content can also be specified via content; in that case, - this function will not load the key from disk. - """ - - try: - if content is None: - with open(path, 'rb') as b_priv_key_fh: - priv_key_detail = b_priv_key_fh.read() - else: - priv_key_detail = content - - if backend == 'pyopenssl': - - # First try: try to load with real passphrase (resp. empty string) - # Will work if this is the correct passphrase, or the key is not - # password-protected. - try: - result = crypto.load_privatekey(crypto.FILETYPE_PEM, - priv_key_detail, - to_bytes(passphrase or '')) - except crypto.Error as e: - if len(e.args) > 0 and len(e.args[0]) > 0: - if e.args[0][0][2] in ('bad decrypt', 'bad password read'): - # This happens in case we have the wrong passphrase. - if passphrase is not None: - raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key!') - else: - raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!') - raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e)) - if check_passphrase: - # Next we want to make sure that the key is actually protected by - # a passphrase (in case we did try the empty string before, make - # sure that the key is not protected by the empty string) - try: - crypto.load_privatekey(crypto.FILETYPE_PEM, - priv_key_detail, - to_bytes('y' if passphrase == 'x' else 'x')) - if passphrase is not None: - # Since we can load the key without an exception, the - # key isn't password-protected - raise OpenSSLBadPassphraseError('Passphrase provided, but private key is not password-protected!') - except crypto.Error as e: - if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0: - if e.args[0][0][2] in ('bad decrypt', 'bad password read'): - # The key is obviously protected by the empty string. - # Don't do this at home (if it's possible at all)... - raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!') - elif backend == 'cryptography': - try: - result = load_pem_private_key(priv_key_detail, - None if passphrase is None else to_bytes(passphrase), - cryptography_backend()) - except TypeError as dummy: - raise OpenSSLBadPassphraseError('Wrong or empty passphrase provided for private key') - except ValueError as dummy: - raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key') - - return result - except (IOError, OSError) as exc: - raise OpenSSLObjectError(exc) - - -def load_certificate(path, content=None, backend='pyopenssl'): - """Load the specified certificate.""" - - try: - if content is None: - with open(path, 'rb') as cert_fh: - cert_content = cert_fh.read() - else: - cert_content = content - if backend == 'pyopenssl': - return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content) - elif backend == 'cryptography': - return x509.load_pem_x509_certificate(cert_content, cryptography_backend()) - except (IOError, OSError) as exc: - raise OpenSSLObjectError(exc) - - -def load_certificate_request(path, content=None, backend='pyopenssl'): - """Load the specified certificate signing request.""" - try: - if content is None: - with open(path, 'rb') as csr_fh: - csr_content = csr_fh.read() - else: - csr_content = content - except (IOError, OSError) as exc: - raise OpenSSLObjectError(exc) - if backend == 'pyopenssl': - return crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_content) - elif backend == 'cryptography': - return x509.load_pem_x509_csr(csr_content, cryptography_backend()) - - -def parse_name_field(input_dict): - """Take a dict with key: value or key: list_of_values mappings and return a list of tuples""" - - result = [] - for key in input_dict: - if isinstance(input_dict[key], list): - for entry in input_dict[key]: - result.append((key, entry)) - else: - result.append((key, input_dict[key])) - return result - - -def convert_relative_to_datetime(relative_time_string): - """Get a datetime.datetime or None from a string in the time format described in sshd_config(5)""" - - parsed_result = re.match( - r"^(?P[+-])((?P\d+)[wW])?((?P\d+)[dD])?((?P\d+)[hH])?((?P\d+)[mM])?((?P\d+)[sS]?)?$", - relative_time_string) - - if parsed_result is None or len(relative_time_string) == 1: - # not matched or only a single "+" or "-" - return None - - offset = datetime.timedelta(0) - if parsed_result.group("weeks") is not None: - offset += datetime.timedelta(weeks=int(parsed_result.group("weeks"))) - if parsed_result.group("days") is not None: - offset += datetime.timedelta(days=int(parsed_result.group("days"))) - if parsed_result.group("hours") is not None: - offset += datetime.timedelta(hours=int(parsed_result.group("hours"))) - if parsed_result.group("minutes") is not None: - offset += datetime.timedelta( - minutes=int(parsed_result.group("minutes"))) - if parsed_result.group("seconds") is not None: - offset += datetime.timedelta( - seconds=int(parsed_result.group("seconds"))) - - if parsed_result.group("prefix") == "+": - return datetime.datetime.utcnow() + offset - else: - return datetime.datetime.utcnow() - offset - - -def get_relative_time_option(input_string, input_name, backend='cryptography'): - """Return an absolute timespec if a relative timespec or an ASN1 formatted - string is provided. - - The return value will be a datetime object for the cryptography backend, - and a ASN1 formatted string for the pyopenssl backend.""" - result = to_native(input_string) - if result is None: - raise OpenSSLObjectError( - 'The timespec "%s" for %s is not valid' % - input_string, input_name) - # Relative time - if result.startswith("+") or result.startswith("-"): - result_datetime = convert_relative_to_datetime(result) - if backend == 'pyopenssl': - return result_datetime.strftime("%Y%m%d%H%M%SZ") - elif backend == 'cryptography': - return result_datetime - # Absolute time - if backend == 'pyopenssl': - return input_string - elif backend == 'cryptography': - for date_fmt in ['%Y%m%d%H%M%SZ', '%Y%m%d%H%MZ', '%Y%m%d%H%M%S%z', '%Y%m%d%H%M%z']: - try: - return datetime.datetime.strptime(result, date_fmt) - except ValueError: - pass - - raise OpenSSLObjectError( - 'The time spec "%s" for %s is invalid' % - (input_string, input_name) - ) - - -def select_message_digest(digest_string): - digest = None - if digest_string == 'sha256': - digest = hashes.SHA256() - elif digest_string == 'sha384': - digest = hashes.SHA384() - elif digest_string == 'sha512': - digest = hashes.SHA512() - elif digest_string == 'sha1': - digest = hashes.SHA1() - elif digest_string == 'md5': - digest = hashes.MD5() - return digest - - -def write_file(module, content, default_mode=None, path=None): - ''' - Writes content into destination file as securely as possible. - Uses file arguments from module. - ''' - # Find out parameters for file - try: - file_args = module.load_file_common_arguments(module.params, path=path) - except TypeError: - # The path argument is only supported in Ansible 2.10+. Fall back to - # pre-2.10 behavior of module_utils/crypto.py for older Ansible versions. - file_args = module.load_file_common_arguments(module.params) - if path is not None: - file_args['path'] = path - if file_args['mode'] is None: - file_args['mode'] = default_mode - # Create tempfile name - tmp_fd, tmp_name = tempfile.mkstemp(prefix=b'.ansible_tmp') - try: - os.close(tmp_fd) - except Exception as dummy: - pass - module.add_cleanup_file(tmp_name) # if we fail, let Ansible try to remove the file - try: - try: - # Create tempfile - file = os.open(tmp_name, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) - os.write(file, content) - os.close(file) - except Exception as e: - try: - os.remove(tmp_name) - except Exception as dummy: - pass - module.fail_json(msg='Error while writing result into temporary file: {0}'.format(e)) - # Update destination to wanted permissions - if os.path.exists(file_args['path']): - module.set_fs_attributes_if_different(file_args, False) - # Move tempfile to final destination - module.atomic_move(tmp_name, file_args['path']) - # Try to update permissions again - module.set_fs_attributes_if_different(file_args, False) - except Exception as e: - try: - os.remove(tmp_name) - except Exception as dummy: - pass - module.fail_json(msg='Error while writing result: {0}'.format(e)) - - -@six.add_metaclass(abc.ABCMeta) -class OpenSSLObject(object): - - def __init__(self, path, state, force, check_mode): - self.path = path - self.state = state - self.force = force - self.name = os.path.basename(path) - self.changed = False - self.check_mode = check_mode - - def check(self, module, perms_required=True): - """Ensure the resource is in its desired state.""" - - def _check_state(): - return os.path.exists(self.path) - - def _check_perms(module): - file_args = module.load_file_common_arguments(module.params) - return not module.set_fs_attributes_if_different(file_args, False) - - if not perms_required: - return _check_state() - - return _check_state() and _check_perms(module) - - @abc.abstractmethod - def dump(self): - """Serialize the object into a dictionary.""" - - pass - - @abc.abstractmethod - def generate(self): - """Generate the resource.""" - - pass - - def remove(self, module): - """Remove the resource from the filesystem.""" - - try: - os.remove(self.path) - self.changed = True - except OSError as exc: - if exc.errno != errno.ENOENT: - raise OpenSSLObjectError(exc) - else: - pass - - -# ##################################################################################### -# ##################################################################################### -# This has been extracted from the OpenSSL project's objects.txt: -# https://github.com/openssl/openssl/blob/9537fe5757bb07761fa275d779bbd40bcf5530e4/crypto/objects/objects.txt -# Extracted with https://gist.github.com/felixfontein/376748017ad65ead093d56a45a5bf376 -# -# In case the following data structure has any copyrightable content, note that it is licensed as follows: -# Copyright (c) the OpenSSL contributors -# Licensed under the Apache License 2.0 -# https://github.com/openssl/openssl/blob/master/LICENSE -_OID_MAP = { +OID_MAP = { '0': ('itu-t', 'ITU-T', 'ccitt'), '0.3.4401.5': ('ntt-ds', ), '0.3.4401.5.3.1.9': ('camellia', ), @@ -1618,514 +1106,3 @@ _OID_MAP = { '2.23.43.1.4.11': ('wap-wsg-idm-ecid-wtls11', ), '2.23.43.1.4.12': ('wap-wsg-idm-ecid-wtls12', ), } -# ##################################################################################### -# ##################################################################################### - -_OID_LOOKUP = dict() -_NORMALIZE_NAMES = dict() -_NORMALIZE_NAMES_SHORT = dict() - -for dotted, names in _OID_MAP.items(): - for name in names: - if name in _NORMALIZE_NAMES and _OID_LOOKUP[name] != dotted: - raise AssertionError( - 'Name collision during setup: "{0}" for OIDs {1} and {2}' - .format(name, dotted, _OID_LOOKUP[name]) - ) - _NORMALIZE_NAMES[name] = names[0] - _NORMALIZE_NAMES_SHORT[name] = names[-1] - _OID_LOOKUP[name] = dotted -for alias, original in [('userID', 'userId')]: - if alias in _NORMALIZE_NAMES: - raise AssertionError( - 'Name collision during adding aliases: "{0}" (alias for "{1}") is already mapped to OID {2}' - .format(alias, original, _OID_LOOKUP[alias]) - ) - _NORMALIZE_NAMES[alias] = original - _NORMALIZE_NAMES_SHORT[alias] = _NORMALIZE_NAMES_SHORT[original] - _OID_LOOKUP[alias] = _OID_LOOKUP[original] - - -def pyopenssl_normalize_name(name, short=False): - nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(name)) - if nid != 0: - b_name = OpenSSL._util.lib.OBJ_nid2ln(nid) - name = to_text(OpenSSL._util.ffi.string(b_name)) - if short: - return _NORMALIZE_NAMES_SHORT.get(name, name) - else: - return _NORMALIZE_NAMES.get(name, name) - - -# ##################################################################################### -# ##################################################################################### -# # This excerpt is dual licensed under the terms of the Apache License, Version -# # 2.0, and the BSD License. See the LICENSE file at -# # https://github.com/pyca/cryptography/blob/master/LICENSE for complete details. -# # -# # Adapted from cryptography's hazmat/backends/openssl/decode_asn1.py -# # -# # Copyright (c) 2015, 2016 Paul Kehrer (@reaperhulk) -# # Copyright (c) 2017 Fraser Tweedale (@frasertweedale) -# # -# # Relevant commits from cryptography project (https://github.com/pyca/cryptography): -# # pyca/cryptography@719d536dd691e84e208534798f2eb4f82aaa2e07 -# # pyca/cryptography@5ab6d6a5c05572bd1c75f05baf264a2d0001894a -# # pyca/cryptography@2e776e20eb60378e0af9b7439000d0e80da7c7e3 -# # pyca/cryptography@fb309ed24647d1be9e319b61b1f2aa8ebb87b90b -# # pyca/cryptography@2917e460993c475c72d7146c50dc3bbc2414280d -# # pyca/cryptography@3057f91ea9a05fb593825006d87a391286a4d828 -# # pyca/cryptography@d607dd7e5bc5c08854ec0c9baff70ba4a35be36f -def _obj2txt(openssl_lib, openssl_ffi, obj): - # Set to 80 on the recommendation of - # https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values - # - # But OIDs longer than this occur in real life (e.g. Active - # Directory makes some very long OIDs). So we need to detect - # and properly handle the case where the default buffer is not - # big enough. - # - buf_len = 80 - buf = openssl_ffi.new("char[]", buf_len) - - # 'res' is the number of bytes that *would* be written if the - # buffer is large enough. If 'res' > buf_len - 1, we need to - # alloc a big-enough buffer and go again. - res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1) - if res > buf_len - 1: # account for terminating null byte - buf_len = res + 1 - buf = openssl_ffi.new("char[]", buf_len) - res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1) - return openssl_ffi.buffer(buf, res)[:].decode() -# ##################################################################################### -# ##################################################################################### - - -def cryptography_get_extensions_from_cert(cert): - # Since cryptography won't give us the DER value for an extension - # (that is only stored for unrecognized extensions), we have to re-do - # the extension parsing outselves. - result = dict() - backend = cert._backend - x509_obj = cert._x509 - - for i in range(backend._lib.X509_get_ext_count(x509_obj)): - ext = backend._lib.X509_get_ext(x509_obj, i) - if ext == backend._ffi.NULL: - continue - crit = backend._lib.X509_EXTENSION_get_critical(ext) - data = backend._lib.X509_EXTENSION_get_data(ext) - backend.openssl_assert(data != backend._ffi.NULL) - der = backend._ffi.buffer(data.data, data.length)[:] - entry = dict( - critical=(crit == 1), - value=base64.b64encode(der), - ) - oid = _obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext)) - result[oid] = entry - return result - - -def cryptography_get_extensions_from_csr(csr): - # Since cryptography won't give us the DER value for an extension - # (that is only stored for unrecognized extensions), we have to re-do - # the extension parsing outselves. - result = dict() - backend = csr._backend - - extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req) - extensions = backend._ffi.gc( - extensions, - lambda ext: backend._lib.sk_X509_EXTENSION_pop_free( - ext, - backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free") - ) - ) - - for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)): - ext = backend._lib.sk_X509_EXTENSION_value(extensions, i) - if ext == backend._ffi.NULL: - continue - crit = backend._lib.X509_EXTENSION_get_critical(ext) - data = backend._lib.X509_EXTENSION_get_data(ext) - backend.openssl_assert(data != backend._ffi.NULL) - der = backend._ffi.buffer(data.data, data.length)[:] - entry = dict( - critical=(crit == 1), - value=base64.b64encode(der), - ) - oid = _obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext)) - result[oid] = entry - return result - - -def pyopenssl_get_extensions_from_cert(cert): - # While pyOpenSSL allows us to get an extension's DER value, it won't - # give us the dotted string for an OID. So we have to do some magic to - # get hold of it. - result = dict() - ext_count = cert.get_extension_count() - for i in range(0, ext_count): - ext = cert.get_extension(i) - entry = dict( - critical=bool(ext.get_critical()), - value=base64.b64encode(ext.get_data()), - ) - oid = _obj2txt( - OpenSSL._util.lib, - OpenSSL._util.ffi, - OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension) - ) - # This could also be done a bit simpler: - # - # oid = _obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid)) - # - # Unfortunately this gives the wrong result in case the linked OpenSSL - # doesn't know the OID. That's why we have to get the OID dotted string - # similarly to how cryptography does it. - result[oid] = entry - return result - - -def pyopenssl_get_extensions_from_csr(csr): - # While pyOpenSSL allows us to get an extension's DER value, it won't - # give us the dotted string for an OID. So we have to do some magic to - # get hold of it. - result = dict() - for ext in csr.get_extensions(): - entry = dict( - critical=bool(ext.get_critical()), - value=base64.b64encode(ext.get_data()), - ) - oid = _obj2txt( - OpenSSL._util.lib, - OpenSSL._util.ffi, - OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension) - ) - # This could also be done a bit simpler: - # - # oid = _obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid)) - # - # Unfortunately this gives the wrong result in case the linked OpenSSL - # doesn't know the OID. That's why we have to get the OID dotted string - # similarly to how cryptography does it. - result[oid] = entry - return result - - -def cryptography_name_to_oid(name): - dotted = _OID_LOOKUP.get(name) - if dotted is None: - raise OpenSSLObjectError('Cannot find OID for "{0}"'.format(name)) - return x509.oid.ObjectIdentifier(dotted) - - -def cryptography_oid_to_name(oid, short=False): - dotted_string = oid.dotted_string - names = _OID_MAP.get(dotted_string) - name = names[0] if names else oid._name - if short: - return _NORMALIZE_NAMES_SHORT.get(name, name) - else: - return _NORMALIZE_NAMES.get(name, name) - - -def cryptography_get_name(name): - ''' - Given a name string, returns a cryptography x509.Name object. - Raises an OpenSSLObjectError if the name is unknown or cannot be parsed. - ''' - try: - if name.startswith('DNS:'): - return x509.DNSName(to_text(name[4:])) - if name.startswith('IP:'): - return x509.IPAddress(ipaddress.ip_address(to_text(name[3:]))) - if name.startswith('email:'): - return x509.RFC822Name(to_text(name[6:])) - if name.startswith('URI:'): - return x509.UniformResourceIdentifier(to_text(name[4:])) - except Exception as e: - raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}": {1}'.format(name, e)) - if ':' not in name: - raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (forgot "DNS:" prefix?)'.format(name)) - raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (potentially unsupported by cryptography backend)'.format(name)) - - -def _get_hex(bytesstr): - if bytesstr is None: - return bytesstr - data = binascii.hexlify(bytesstr) - data = to_text(b':'.join(data[i:i + 2] for i in range(0, len(data), 2))) - return data - - -def cryptography_decode_name(name): - ''' - Given a cryptography x509.Name object, returns a string. - Raises an OpenSSLObjectError if the name is not supported. - ''' - if isinstance(name, x509.DNSName): - return 'DNS:{0}'.format(name.value) - if isinstance(name, x509.IPAddress): - return 'IP:{0}'.format(name.value.compressed) - if isinstance(name, x509.RFC822Name): - return 'email:{0}'.format(name.value) - if isinstance(name, x509.UniformResourceIdentifier): - return 'URI:{0}'.format(name.value) - if isinstance(name, x509.DirectoryName): - # FIXME: test - return 'DirName:' + ''.join(['/{0}:{1}'.format(attribute.oid._name, attribute.value) for attribute in name.value]) - if isinstance(name, x509.RegisteredID): - # FIXME: test - return 'RegisteredID:{0}'.format(name.value) - if isinstance(name, x509.OtherName): - # FIXME: test - return '{0}:{1}'.format(name.type_id.dotted_string, _get_hex(name.value)) - raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name)) - - -def _cryptography_get_keyusage(usage): - ''' - Given a key usage identifier string, returns the parameter name used by cryptography's x509.KeyUsage(). - Raises an OpenSSLObjectError if the identifier is unknown. - ''' - if usage in ('Digital Signature', 'digitalSignature'): - return 'digital_signature' - if usage in ('Non Repudiation', 'nonRepudiation'): - return 'content_commitment' - if usage in ('Key Encipherment', 'keyEncipherment'): - return 'key_encipherment' - if usage in ('Data Encipherment', 'dataEncipherment'): - return 'data_encipherment' - if usage in ('Key Agreement', 'keyAgreement'): - return 'key_agreement' - if usage in ('Certificate Sign', 'keyCertSign'): - return 'key_cert_sign' - if usage in ('CRL Sign', 'cRLSign'): - return 'crl_sign' - if usage in ('Encipher Only', 'encipherOnly'): - return 'encipher_only' - if usage in ('Decipher Only', 'decipherOnly'): - return 'decipher_only' - raise OpenSSLObjectError('Unknown key usage "{0}"'.format(usage)) - - -def cryptography_parse_key_usage_params(usages): - ''' - Given a list of key usage identifier strings, returns the parameters for cryptography's x509.KeyUsage(). - Raises an OpenSSLObjectError if an identifier is unknown. - ''' - params = dict( - digital_signature=False, - content_commitment=False, - key_encipherment=False, - data_encipherment=False, - key_agreement=False, - key_cert_sign=False, - crl_sign=False, - encipher_only=False, - decipher_only=False, - ) - for usage in usages: - params[_cryptography_get_keyusage(usage)] = True - return params - - -def cryptography_get_basic_constraints(constraints): - ''' - Given a list of constraints, returns a tuple (ca, path_length). - Raises an OpenSSLObjectError if a constraint is unknown or cannot be parsed. - ''' - ca = False - path_length = None - if constraints: - for constraint in constraints: - if constraint.startswith('CA:'): - if constraint == 'CA:TRUE': - ca = True - elif constraint == 'CA:FALSE': - ca = False - else: - raise OpenSSLObjectError('Unknown basic constraint value "{0}" for CA'.format(constraint[3:])) - elif constraint.startswith('pathlen:'): - v = constraint[len('pathlen:'):] - try: - path_length = int(v) - except Exception as e: - raise OpenSSLObjectError('Cannot parse path length constraint "{0}" ({1})'.format(v, e)) - else: - raise OpenSSLObjectError('Unknown basic constraint "{0}"'.format(constraint)) - return ca, path_length - - -def binary_exp_mod(f, e, m): - '''Computes f^e mod m in O(log e) multiplications modulo m.''' - # Compute len_e = floor(log_2(e)) - len_e = -1 - x = e - while x > 0: - x >>= 1 - len_e += 1 - # Compute f**e mod m - result = 1 - for k in range(len_e, -1, -1): - result = (result * result) % m - if ((e >> k) & 1) != 0: - result = (result * f) % m - return result - - -def simple_gcd(a, b): - '''Compute GCD of its two inputs.''' - while b != 0: - a, b = b, a % b - return a - - -def quick_is_not_prime(n): - '''Does some quick checks to see if we can poke a hole into the primality of n. - - A result of `False` does **not** mean that the number is prime; it just means - that we couldn't detect quickly whether it is not prime. - ''' - if n <= 2: - return True - # The constant in the next line is the product of all primes < 200 - if simple_gcd(n, 7799922041683461553249199106329813876687996789903550945093032474868511536164700810) > 1: - return True - # TODO: maybe do some iterations of Miller-Rabin to increase confidence - # (https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test) - 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_bits(no): - no = abs(no) - if no == 0: - return 0 - return no.bit_length() -else: - # Slow, but works - def count_bits(no): - no = abs(no) - count = 0 - while no > 0: - no >>= 1 - count += 1 - return count - - -PEM_START = '-----BEGIN ' -PEM_END = '-----' -PKCS8_PRIVATEKEY_NAMES = ('PRIVATE KEY', 'ENCRYPTED PRIVATE KEY') -PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY' - - -def identify_private_key_format(content): - '''Given the contents of a private key file, identifies its format.''' - # See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85 - # (PEM_read_bio_PrivateKey) - # and https://github.com/openssl/openssl/blob/master/include/openssl/pem.h#L46-L47 - # (PEM_STRING_PKCS8, PEM_STRING_PKCS8INF) - try: - lines = content.decode('utf-8').splitlines(False) - if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END): - name = lines[0][len(PEM_START):-len(PEM_END)] - if name in PKCS8_PRIVATEKEY_NAMES: - return 'pkcs8' - if len(name) > len(PKCS1_PRIVATEKEY_SUFFIX) and name.endswith(PKCS1_PRIVATEKEY_SUFFIX): - return 'pkcs1' - return 'unknown-pem' - except UnicodeDecodeError: - pass - return 'raw' - - -def cryptography_key_needs_digest_for_signing(key): - '''Tests whether the given private key requires a digest algorithm for signing. - - Ed25519 and Ed448 keys do not; they need None to be passed as the digest algorithm. - ''' - if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): - return False - if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): - return False - return True - - -def cryptography_compare_public_keys(key1, key2): - '''Tests whether two public keys are the same. - - Needs special logic for Ed25519 and Ed448 keys, since they do not have public_numbers(). - ''' - if CRYPTOGRAPHY_HAS_ED25519: - a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey) - b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey) - if a or b: - if not a or not b: - return False - a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) - b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) - return a == b - if CRYPTOGRAPHY_HAS_ED448: - a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey) - b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey) - if a or b: - if not a or not b: - return False - a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) - b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) - return a == b - return key1.public_numbers() == key2.public_numbers() - - -if HAS_CRYPTOGRAPHY: - REVOCATION_REASON_MAP = { - 'unspecified': x509.ReasonFlags.unspecified, - 'key_compromise': x509.ReasonFlags.key_compromise, - 'ca_compromise': x509.ReasonFlags.ca_compromise, - 'affiliation_changed': x509.ReasonFlags.affiliation_changed, - 'superseded': x509.ReasonFlags.superseded, - 'cessation_of_operation': x509.ReasonFlags.cessation_of_operation, - 'certificate_hold': x509.ReasonFlags.certificate_hold, - 'privilege_withdrawn': x509.ReasonFlags.privilege_withdrawn, - 'aa_compromise': x509.ReasonFlags.aa_compromise, - 'remove_from_crl': x509.ReasonFlags.remove_from_crl, - } - REVOCATION_REASON_MAP_INVERSE = dict() - for k, v in REVOCATION_REASON_MAP.items(): - REVOCATION_REASON_MAP_INVERSE[v] = k - - -def cryptography_decode_revoked_certificate(cert): - result = { - 'serial_number': cert.serial_number, - 'revocation_date': cert.revocation_date, - 'issuer': None, - 'issuer_critical': False, - 'reason': None, - 'reason_critical': False, - 'invalidity_date': None, - 'invalidity_date_critical': False, - } - try: - ext = cert.extensions.get_extension_for_class(x509.CertificateIssuer) - result['issuer'] = list(ext.value) - result['issuer_critical'] = ext.critical - except x509.ExtensionNotFound: - pass - try: - ext = cert.extensions.get_extension_for_class(x509.CRLReason) - result['reason'] = ext.value.reason - result['reason_critical'] = ext.critical - except x509.ExtensionNotFound: - pass - try: - ext = cert.extensions.get_extension_for_class(x509.InvalidityDate) - result['invalidity_date'] = ext.value.invalidity_date - result['invalidity_date_critical'] = ext.critical - except x509.ExtensionNotFound: - pass - return result diff --git a/plugins/module_utils/crypto/basic.py b/plugins/module_utils/crypto/basic.py new file mode 100644 index 00000000..f91ec90a --- /dev/null +++ b/plugins/module_utils/crypto/basic.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +# +# (c) 2016, Yanis Guenane +# (c) 2020, Felix Fontein +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +from distutils.version import LooseVersion + +try: + import OpenSSL # noqa + from OpenSSL import crypto # noqa + HAS_PYOPENSSL = True +except ImportError: + # Error handled in the calling module. + HAS_PYOPENSSL = False + +try: + import cryptography + from cryptography import x509 + + # Older versions of cryptography (< 2.1) do not have __hash__ functions for + # general name objects (DNSName, IPAddress, ...), while providing overloaded + # equality and string representation operations. This makes it impossible to + # use them in hash-based data structures such as set or dict. Since we are + # actually doing that in x509_certificate, and potentially in other code, + # we need to monkey-patch __hash__ for these classes to make sure our code + # works fine. + if LooseVersion(cryptography.__version__) < LooseVersion('2.1'): + # A very simply hash function which relies on the representation + # of an object to be implemented. This is the case since at least + # cryptography 1.0, see + # https://github.com/pyca/cryptography/commit/7a9abce4bff36c05d26d8d2680303a6f64a0e84f + def simple_hash(self): + return hash(repr(self)) + + # The hash functions for the following types were added for cryptography 2.1: + # https://github.com/pyca/cryptography/commit/fbfc36da2a4769045f2373b004ddf0aff906cf38 + x509.DNSName.__hash__ = simple_hash + x509.DirectoryName.__hash__ = simple_hash + x509.GeneralName.__hash__ = simple_hash + x509.IPAddress.__hash__ = simple_hash + x509.OtherName.__hash__ = simple_hash + x509.RegisteredID.__hash__ = simple_hash + + if LooseVersion(cryptography.__version__) < LooseVersion('1.2'): + # The hash functions for the following types were added for cryptography 1.2: + # https://github.com/pyca/cryptography/commit/b642deed88a8696e5f01ce6855ccf89985fc35d0 + # https://github.com/pyca/cryptography/commit/d1b5681f6db2bde7a14625538bd7907b08dfb486 + x509.RFC822Name.__hash__ = simple_hash + x509.UniformResourceIdentifier.__hash__ = simple_hash + + # Test whether we have support for X25519, X448, Ed25519 and/or Ed448 + try: + import cryptography.hazmat.primitives.asymmetric.x25519 + CRYPTOGRAPHY_HAS_X25519 = True + try: + cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.private_bytes + CRYPTOGRAPHY_HAS_X25519_FULL = True + except AttributeError: + CRYPTOGRAPHY_HAS_X25519_FULL = False + except ImportError: + CRYPTOGRAPHY_HAS_X25519 = False + CRYPTOGRAPHY_HAS_X25519_FULL = False + try: + import cryptography.hazmat.primitives.asymmetric.x448 + CRYPTOGRAPHY_HAS_X448 = True + except ImportError: + CRYPTOGRAPHY_HAS_X448 = False + try: + import cryptography.hazmat.primitives.asymmetric.ed25519 + CRYPTOGRAPHY_HAS_ED25519 = True + except ImportError: + CRYPTOGRAPHY_HAS_ED25519 = False + try: + import cryptography.hazmat.primitives.asymmetric.ed448 + CRYPTOGRAPHY_HAS_ED448 = True + except ImportError: + CRYPTOGRAPHY_HAS_ED448 = False + + HAS_CRYPTOGRAPHY = True +except ImportError: + # Error handled in the calling module. + CRYPTOGRAPHY_HAS_X25519 = False + CRYPTOGRAPHY_HAS_X25519_FULL = False + CRYPTOGRAPHY_HAS_X448 = False + CRYPTOGRAPHY_HAS_ED25519 = False + CRYPTOGRAPHY_HAS_ED448 = False + HAS_CRYPTOGRAPHY = False + + +class OpenSSLObjectError(Exception): + pass + + +class OpenSSLBadPassphraseError(OpenSSLObjectError): + pass diff --git a/plugins/module_utils/crypto/cryptography_crl.py b/plugins/module_utils/crypto/cryptography_crl.py new file mode 100644 index 00000000..aa73b512 --- /dev/null +++ b/plugins/module_utils/crypto/cryptography_crl.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# +# (c) 2019, Felix Fontein +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +try: + from cryptography import x509 +except ImportError: + # Error handled in the calling module. + pass + +from .basic import ( + HAS_CRYPTOGRAPHY, +) + +from .cryptography_support import ( + cryptography_decode_name, +) + +from ._obj2txt import ( + obj2txt, +) + + +TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" + + +if HAS_CRYPTOGRAPHY: + REVOCATION_REASON_MAP = { + 'unspecified': x509.ReasonFlags.unspecified, + 'key_compromise': x509.ReasonFlags.key_compromise, + 'ca_compromise': x509.ReasonFlags.ca_compromise, + 'affiliation_changed': x509.ReasonFlags.affiliation_changed, + 'superseded': x509.ReasonFlags.superseded, + 'cessation_of_operation': x509.ReasonFlags.cessation_of_operation, + 'certificate_hold': x509.ReasonFlags.certificate_hold, + 'privilege_withdrawn': x509.ReasonFlags.privilege_withdrawn, + 'aa_compromise': x509.ReasonFlags.aa_compromise, + 'remove_from_crl': x509.ReasonFlags.remove_from_crl, + } + REVOCATION_REASON_MAP_INVERSE = dict() + for k, v in REVOCATION_REASON_MAP.items(): + REVOCATION_REASON_MAP_INVERSE[v] = k + +else: + REVOCATION_REASON_MAP = dict() + REVOCATION_REASON_MAP_INVERSE = dict() + + +def cryptography_decode_revoked_certificate(cert): + result = { + 'serial_number': cert.serial_number, + 'revocation_date': cert.revocation_date, + 'issuer': None, + 'issuer_critical': False, + 'reason': None, + 'reason_critical': False, + 'invalidity_date': None, + 'invalidity_date_critical': False, + } + try: + ext = cert.extensions.get_extension_for_class(x509.CertificateIssuer) + result['issuer'] = list(ext.value) + result['issuer_critical'] = ext.critical + except x509.ExtensionNotFound: + pass + try: + ext = cert.extensions.get_extension_for_class(x509.CRLReason) + result['reason'] = ext.value.reason + result['reason_critical'] = ext.critical + except x509.ExtensionNotFound: + pass + try: + ext = cert.extensions.get_extension_for_class(x509.InvalidityDate) + result['invalidity_date'] = ext.value.invalidity_date + result['invalidity_date_critical'] = ext.critical + except x509.ExtensionNotFound: + pass + return result + + +def cryptography_dump_revoked(entry): + return { + 'serial_number': entry['serial_number'], + 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT), + 'issuer': + [cryptography_decode_name(issuer) for issuer in entry['issuer']] + if entry['issuer'] is not None else None, + 'issuer_critical': entry['issuer_critical'], + 'reason': REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None, + 'reason_critical': entry['reason_critical'], + 'invalidity_date': + entry['invalidity_date'].strftime(TIMESTAMP_FORMAT) + if entry['invalidity_date'] is not None else None, + 'invalidity_date_critical': entry['invalidity_date_critical'], + } + + +def cryptography_get_signature_algorithm_oid_from_crl(crl): + try: + return crl.signature_algorithm_oid + except AttributeError: + # Older cryptography versions don't have signature_algorithm_oid yet + dotted = obj2txt( + crl._backend._lib, + crl._backend._ffi, + crl._x509_crl.sig_alg.algorithm + ) + return x509.oid.ObjectIdentifier(dotted) diff --git a/plugins/module_utils/crypto/cryptography_support.py b/plugins/module_utils/crypto/cryptography_support.py new file mode 100644 index 00000000..109e567a --- /dev/null +++ b/plugins/module_utils/crypto/cryptography_support.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +# +# (c) 2019, Felix Fontein +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import base64 +import binascii + +from ansible.module_utils._text import to_text + +try: + import cryptography + from cryptography import x509 + from cryptography.hazmat.primitives import serialization + import ipaddress +except ImportError: + # Error handled in the calling module. + pass + +from .basic import ( + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED448, + OpenSSLObjectError, +) + +from ._objects import ( + OID_LOOKUP, + OID_MAP, + NORMALIZE_NAMES_SHORT, + NORMALIZE_NAMES, +) + +from ._obj2txt import obj2txt + + +def cryptography_get_extensions_from_cert(cert): + # Since cryptography won't give us the DER value for an extension + # (that is only stored for unrecognized extensions), we have to re-do + # the extension parsing outselves. + result = dict() + backend = cert._backend + x509_obj = cert._x509 + + for i in range(backend._lib.X509_get_ext_count(x509_obj)): + ext = backend._lib.X509_get_ext(x509_obj, i) + if ext == backend._ffi.NULL: + continue + crit = backend._lib.X509_EXTENSION_get_critical(ext) + data = backend._lib.X509_EXTENSION_get_data(ext) + backend.openssl_assert(data != backend._ffi.NULL) + der = backend._ffi.buffer(data.data, data.length)[:] + entry = dict( + critical=(crit == 1), + value=base64.b64encode(der), + ) + oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext)) + result[oid] = entry + return result + + +def cryptography_get_extensions_from_csr(csr): + # Since cryptography won't give us the DER value for an extension + # (that is only stored for unrecognized extensions), we have to re-do + # the extension parsing outselves. + result = dict() + backend = csr._backend + + extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req) + extensions = backend._ffi.gc( + extensions, + lambda ext: backend._lib.sk_X509_EXTENSION_pop_free( + ext, + backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free") + ) + ) + + for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)): + ext = backend._lib.sk_X509_EXTENSION_value(extensions, i) + if ext == backend._ffi.NULL: + continue + crit = backend._lib.X509_EXTENSION_get_critical(ext) + data = backend._lib.X509_EXTENSION_get_data(ext) + backend.openssl_assert(data != backend._ffi.NULL) + der = backend._ffi.buffer(data.data, data.length)[:] + entry = dict( + critical=(crit == 1), + value=base64.b64encode(der), + ) + oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext)) + result[oid] = entry + return result + + +def cryptography_name_to_oid(name): + dotted = OID_LOOKUP.get(name) + if dotted is None: + raise OpenSSLObjectError('Cannot find OID for "{0}"'.format(name)) + return x509.oid.ObjectIdentifier(dotted) + + +def cryptography_oid_to_name(oid, short=False): + dotted_string = oid.dotted_string + names = OID_MAP.get(dotted_string) + name = names[0] if names else oid._name + if short: + return NORMALIZE_NAMES_SHORT.get(name, name) + else: + return NORMALIZE_NAMES.get(name, name) + + +def cryptography_get_name(name): + ''' + Given a name string, returns a cryptography x509.Name object. + Raises an OpenSSLObjectError if the name is unknown or cannot be parsed. + ''' + try: + if name.startswith('DNS:'): + return x509.DNSName(to_text(name[4:])) + if name.startswith('IP:'): + return x509.IPAddress(ipaddress.ip_address(to_text(name[3:]))) + if name.startswith('email:'): + return x509.RFC822Name(to_text(name[6:])) + if name.startswith('URI:'): + return x509.UniformResourceIdentifier(to_text(name[4:])) + except Exception as e: + raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}": {1}'.format(name, e)) + if ':' not in name: + raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (forgot "DNS:" prefix?)'.format(name)) + raise OpenSSLObjectError('Cannot parse Subject Alternative Name "{0}" (potentially unsupported by cryptography backend)'.format(name)) + + +def _get_hex(bytesstr): + if bytesstr is None: + return bytesstr + data = binascii.hexlify(bytesstr) + data = to_text(b':'.join(data[i:i + 2] for i in range(0, len(data), 2))) + return data + + +def cryptography_decode_name(name): + ''' + Given a cryptography x509.Name object, returns a string. + Raises an OpenSSLObjectError if the name is not supported. + ''' + if isinstance(name, x509.DNSName): + return 'DNS:{0}'.format(name.value) + if isinstance(name, x509.IPAddress): + return 'IP:{0}'.format(name.value.compressed) + if isinstance(name, x509.RFC822Name): + return 'email:{0}'.format(name.value) + if isinstance(name, x509.UniformResourceIdentifier): + return 'URI:{0}'.format(name.value) + if isinstance(name, x509.DirectoryName): + # FIXME: test + return 'DirName:' + ''.join(['/{0}:{1}'.format(attribute.oid._name, attribute.value) for attribute in name.value]) + if isinstance(name, x509.RegisteredID): + # FIXME: test + return 'RegisteredID:{0}'.format(name.value) + if isinstance(name, x509.OtherName): + # FIXME: test + return '{0}:{1}'.format(name.type_id.dotted_string, _get_hex(name.value)) + raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name)) + + +def _cryptography_get_keyusage(usage): + ''' + Given a key usage identifier string, returns the parameter name used by cryptography's x509.KeyUsage(). + Raises an OpenSSLObjectError if the identifier is unknown. + ''' + if usage in ('Digital Signature', 'digitalSignature'): + return 'digital_signature' + if usage in ('Non Repudiation', 'nonRepudiation'): + return 'content_commitment' + if usage in ('Key Encipherment', 'keyEncipherment'): + return 'key_encipherment' + if usage in ('Data Encipherment', 'dataEncipherment'): + return 'data_encipherment' + if usage in ('Key Agreement', 'keyAgreement'): + return 'key_agreement' + if usage in ('Certificate Sign', 'keyCertSign'): + return 'key_cert_sign' + if usage in ('CRL Sign', 'cRLSign'): + return 'crl_sign' + if usage in ('Encipher Only', 'encipherOnly'): + return 'encipher_only' + if usage in ('Decipher Only', 'decipherOnly'): + return 'decipher_only' + raise OpenSSLObjectError('Unknown key usage "{0}"'.format(usage)) + + +def cryptography_parse_key_usage_params(usages): + ''' + Given a list of key usage identifier strings, returns the parameters for cryptography's x509.KeyUsage(). + Raises an OpenSSLObjectError if an identifier is unknown. + ''' + params = dict( + digital_signature=False, + content_commitment=False, + key_encipherment=False, + data_encipherment=False, + key_agreement=False, + key_cert_sign=False, + crl_sign=False, + encipher_only=False, + decipher_only=False, + ) + for usage in usages: + params[_cryptography_get_keyusage(usage)] = True + return params + + +def cryptography_get_basic_constraints(constraints): + ''' + Given a list of constraints, returns a tuple (ca, path_length). + Raises an OpenSSLObjectError if a constraint is unknown or cannot be parsed. + ''' + ca = False + path_length = None + if constraints: + for constraint in constraints: + if constraint.startswith('CA:'): + if constraint == 'CA:TRUE': + ca = True + elif constraint == 'CA:FALSE': + ca = False + else: + raise OpenSSLObjectError('Unknown basic constraint value "{0}" for CA'.format(constraint[3:])) + elif constraint.startswith('pathlen:'): + v = constraint[len('pathlen:'):] + try: + path_length = int(v) + except Exception as e: + raise OpenSSLObjectError('Cannot parse path length constraint "{0}" ({1})'.format(v, e)) + else: + raise OpenSSLObjectError('Unknown basic constraint "{0}"'.format(constraint)) + return ca, path_length + + +def cryptography_key_needs_digest_for_signing(key): + '''Tests whether the given private key requires a digest algorithm for signing. + + Ed25519 and Ed448 keys do not; they need None to be passed as the digest algorithm. + ''' + if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): + return False + if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): + return False + return True + + +def cryptography_compare_public_keys(key1, key2): + '''Tests whether two public keys are the same. + + Needs special logic for Ed25519 and Ed448 keys, since they do not have public_numbers(). + ''' + if CRYPTOGRAPHY_HAS_ED25519: + a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey) + b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey) + if a or b: + if not a or not b: + return False + a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + return a == b + if CRYPTOGRAPHY_HAS_ED448: + a = isinstance(key1, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey) + b = isinstance(key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey) + if a or b: + if not a or not b: + return False + a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw) + return a == b + return key1.public_numbers() == key2.public_numbers() + + +def cryptography_serial_number_of_cert(cert): + '''Returns cert.serial_number. + + Also works for old versions of cryptography. + ''' + try: + return cert.serial_number + except AttributeError: + # The property was called "serial" before cryptography 1.4 + return cert.serial diff --git a/plugins/module_utils/crypto/identify.py b/plugins/module_utils/crypto/identify.py new file mode 100644 index 00000000..8d790f8e --- /dev/null +++ b/plugins/module_utils/crypto/identify.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# +# (c) 2019, Felix Fontein +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +PEM_START = '-----BEGIN ' +PEM_END = '-----' +PKCS8_PRIVATEKEY_NAMES = ('PRIVATE KEY', 'ENCRYPTED PRIVATE KEY') +PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY' + + +def identify_private_key_format(content): + '''Given the contents of a private key file, identifies its format.''' + # See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85 + # (PEM_read_bio_PrivateKey) + # and https://github.com/openssl/openssl/blob/master/include/openssl/pem.h#L46-L47 + # (PEM_STRING_PKCS8, PEM_STRING_PKCS8INF) + try: + lines = content.decode('utf-8').splitlines(False) + if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END): + name = lines[0][len(PEM_START):-len(PEM_END)] + if name in PKCS8_PRIVATEKEY_NAMES: + return 'pkcs8' + if len(name) > len(PKCS1_PRIVATEKEY_SUFFIX) and name.endswith(PKCS1_PRIVATEKEY_SUFFIX): + return 'pkcs1' + return 'unknown-pem' + except UnicodeDecodeError: + pass + return 'raw' diff --git a/plugins/module_utils/crypto/math.py b/plugins/module_utils/crypto/math.py new file mode 100644 index 00000000..c98eb1ed --- /dev/null +++ b/plugins/module_utils/crypto/math.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- +# +# (c) 2019, Felix Fontein +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import sys + + +def binary_exp_mod(f, e, m): + '''Computes f^e mod m in O(log e) multiplications modulo m.''' + # Compute len_e = floor(log_2(e)) + len_e = -1 + x = e + while x > 0: + x >>= 1 + len_e += 1 + # Compute f**e mod m + result = 1 + for k in range(len_e, -1, -1): + result = (result * result) % m + if ((e >> k) & 1) != 0: + result = (result * f) % m + return result + + +def simple_gcd(a, b): + '''Compute GCD of its two inputs.''' + while b != 0: + a, b = b, a % b + return a + + +def quick_is_not_prime(n): + '''Does some quick checks to see if we can poke a hole into the primality of n. + + A result of `False` does **not** mean that the number is prime; it just means + that we couldn't detect quickly whether it is not prime. + ''' + if n <= 2: + return True + # The constant in the next line is the product of all primes < 200 + if simple_gcd(n, 7799922041683461553249199106329813876687996789903550945093032474868511536164700810) > 1: + return True + # TODO: maybe do some iterations of Miller-Rabin to increase confidence + # (https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test) + 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_bits(no): + no = abs(no) + if no == 0: + return 0 + return no.bit_length() +else: + # Slow, but works + def count_bits(no): + no = abs(no) + count = 0 + while no > 0: + no >>= 1 + count += 1 + return count diff --git a/plugins/module_utils/crypto/pyopenssl_support.py b/plugins/module_utils/crypto/pyopenssl_support.py new file mode 100644 index 00000000..4c95e5ea --- /dev/null +++ b/plugins/module_utils/crypto/pyopenssl_support.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# +# (c) 2019, Felix Fontein +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import base64 + +from ansible.module_utils._text import to_bytes, to_text + +try: + import OpenSSL +except ImportError: + # Error handled in the calling module. + pass + +from ._objects import ( + NORMALIZE_NAMES_SHORT, + NORMALIZE_NAMES, +) + +from ._obj2txt import obj2txt + + +def pyopenssl_normalize_name(name, short=False): + nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(name)) + if nid != 0: + b_name = OpenSSL._util.lib.OBJ_nid2ln(nid) + name = to_text(OpenSSL._util.ffi.string(b_name)) + if short: + return NORMALIZE_NAMES_SHORT.get(name, name) + else: + return NORMALIZE_NAMES.get(name, name) + + +def pyopenssl_get_extensions_from_cert(cert): + # While pyOpenSSL allows us to get an extension's DER value, it won't + # give us the dotted string for an OID. So we have to do some magic to + # get hold of it. + result = dict() + ext_count = cert.get_extension_count() + for i in range(0, ext_count): + ext = cert.get_extension(i) + entry = dict( + critical=bool(ext.get_critical()), + value=base64.b64encode(ext.get_data()), + ) + oid = obj2txt( + OpenSSL._util.lib, + OpenSSL._util.ffi, + OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension) + ) + # This could also be done a bit simpler: + # + # oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid)) + # + # Unfortunately this gives the wrong result in case the linked OpenSSL + # doesn't know the OID. That's why we have to get the OID dotted string + # similarly to how cryptography does it. + result[oid] = entry + return result + + +def pyopenssl_get_extensions_from_csr(csr): + # While pyOpenSSL allows us to get an extension's DER value, it won't + # give us the dotted string for an OID. So we have to do some magic to + # get hold of it. + result = dict() + for ext in csr.get_extensions(): + entry = dict( + critical=bool(ext.get_critical()), + value=base64.b64encode(ext.get_data()), + ) + oid = obj2txt( + OpenSSL._util.lib, + OpenSSL._util.ffi, + OpenSSL._util.lib.X509_EXTENSION_get_object(ext._extension) + ) + # This could also be done a bit simpler: + # + # oid = obj2txt(OpenSSL._util.lib, OpenSSL._util.ffi, OpenSSL._util.lib.OBJ_nid2obj(ext._nid)) + # + # Unfortunately this gives the wrong result in case the linked OpenSSL + # doesn't know the OID. That's why we have to get the OID dotted string + # similarly to how cryptography does it. + result[oid] = entry + return result diff --git a/plugins/module_utils/crypto/support.py b/plugins/module_utils/crypto/support.py new file mode 100644 index 00000000..a84dd4f4 --- /dev/null +++ b/plugins/module_utils/crypto/support.py @@ -0,0 +1,354 @@ +# -*- coding: utf-8 -*- +# +# (c) 2016, Yanis Guenane +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import abc +import datetime +import errno +import hashlib +import os +import re + +from ansible.module_utils import six +from ansible.module_utils._text import to_native, to_bytes + +try: + from OpenSSL import crypto + HAS_PYOPENSSL = True +except ImportError: + # Error handled in the calling module. + HAS_PYOPENSSL = False + +try: + from cryptography import x509 + from cryptography.hazmat.backends import default_backend as cryptography_backend + from cryptography.hazmat.primitives.serialization import load_pem_private_key + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.primitives import serialization +except ImportError: + # Error handled in the calling module. + pass + +from .basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + + +def get_fingerprint_of_bytes(source): + """Generate the fingerprint of the given bytes.""" + + fingerprint = {} + + try: + algorithms = hashlib.algorithms + except AttributeError: + try: + algorithms = hashlib.algorithms_guaranteed + except AttributeError: + return None + + for algo in algorithms: + f = getattr(hashlib, algo) + try: + h = f(source) + except ValueError: + # This can happen for hash algorithms not supported in FIPS mode + # (https://github.com/ansible/ansible/issues/67213) + continue + try: + # Certain hash functions have a hexdigest() which expects a length parameter + pubkey_digest = h.hexdigest() + except TypeError: + pubkey_digest = h.hexdigest(32) + fingerprint[algo] = ':'.join(pubkey_digest[i:i + 2] for i in range(0, len(pubkey_digest), 2)) + + return fingerprint + + +def get_fingerprint(path, passphrase=None, content=None, backend='pyopenssl'): + """Generate the fingerprint of the public key. """ + + privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend) + + if backend == 'pyopenssl': + try: + publickey = crypto.dump_publickey(crypto.FILETYPE_ASN1, privatekey) + except AttributeError: + # If PyOpenSSL < 16.0 crypto.dump_publickey() will fail. + try: + bio = crypto._new_mem_buf() + rc = crypto._lib.i2d_PUBKEY_bio(bio, privatekey._pkey) + if rc != 1: + crypto._raise_current_error() + publickey = crypto._bio_to_string(bio) + except AttributeError: + # By doing this we prevent the code from raising an error + # yet we return no value in the fingerprint hash. + return None + elif backend == 'cryptography': + publickey = privatekey.public_key().public_bytes( + serialization.Encoding.DER, + serialization.PublicFormat.SubjectPublicKeyInfo + ) + + return get_fingerprint_of_bytes(publickey) + + +def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='pyopenssl'): + """Load the specified OpenSSL private key. + + The content can also be specified via content; in that case, + this function will not load the key from disk. + """ + + try: + if content is None: + with open(path, 'rb') as b_priv_key_fh: + priv_key_detail = b_priv_key_fh.read() + else: + priv_key_detail = content + + if backend == 'pyopenssl': + + # First try: try to load with real passphrase (resp. empty string) + # Will work if this is the correct passphrase, or the key is not + # password-protected. + try: + result = crypto.load_privatekey(crypto.FILETYPE_PEM, + priv_key_detail, + to_bytes(passphrase or '')) + except crypto.Error as e: + if len(e.args) > 0 and len(e.args[0]) > 0: + if e.args[0][0][2] in ('bad decrypt', 'bad password read'): + # This happens in case we have the wrong passphrase. + if passphrase is not None: + raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key!') + else: + raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!') + raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e)) + if check_passphrase: + # Next we want to make sure that the key is actually protected by + # a passphrase (in case we did try the empty string before, make + # sure that the key is not protected by the empty string) + try: + crypto.load_privatekey(crypto.FILETYPE_PEM, + priv_key_detail, + to_bytes('y' if passphrase == 'x' else 'x')) + if passphrase is not None: + # Since we can load the key without an exception, the + # key isn't password-protected + raise OpenSSLBadPassphraseError('Passphrase provided, but private key is not password-protected!') + except crypto.Error as e: + if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0: + if e.args[0][0][2] in ('bad decrypt', 'bad password read'): + # The key is obviously protected by the empty string. + # Don't do this at home (if it's possible at all)... + raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!') + elif backend == 'cryptography': + try: + result = load_pem_private_key(priv_key_detail, + None if passphrase is None else to_bytes(passphrase), + cryptography_backend()) + except TypeError: + raise OpenSSLBadPassphraseError('Wrong or empty passphrase provided for private key') + except ValueError: + raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key') + + return result + except (IOError, OSError) as exc: + raise OpenSSLObjectError(exc) + + +def load_certificate(path, content=None, backend='pyopenssl'): + """Load the specified certificate.""" + + try: + if content is None: + with open(path, 'rb') as cert_fh: + cert_content = cert_fh.read() + else: + cert_content = content + if backend == 'pyopenssl': + return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content) + elif backend == 'cryptography': + return x509.load_pem_x509_certificate(cert_content, cryptography_backend()) + except (IOError, OSError) as exc: + raise OpenSSLObjectError(exc) + + +def load_certificate_request(path, content=None, backend='pyopenssl'): + """Load the specified certificate signing request.""" + try: + if content is None: + with open(path, 'rb') as csr_fh: + csr_content = csr_fh.read() + else: + csr_content = content + except (IOError, OSError) as exc: + raise OpenSSLObjectError(exc) + if backend == 'pyopenssl': + return crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_content) + elif backend == 'cryptography': + return x509.load_pem_x509_csr(csr_content, cryptography_backend()) + + +def parse_name_field(input_dict): + """Take a dict with key: value or key: list_of_values mappings and return a list of tuples""" + + result = [] + for key in input_dict: + if isinstance(input_dict[key], list): + for entry in input_dict[key]: + result.append((key, entry)) + else: + result.append((key, input_dict[key])) + return result + + +def convert_relative_to_datetime(relative_time_string): + """Get a datetime.datetime or None from a string in the time format described in sshd_config(5)""" + + parsed_result = re.match( + r"^(?P[+-])((?P\d+)[wW])?((?P\d+)[dD])?((?P\d+)[hH])?((?P\d+)[mM])?((?P\d+)[sS]?)?$", + relative_time_string) + + if parsed_result is None or len(relative_time_string) == 1: + # not matched or only a single "+" or "-" + return None + + offset = datetime.timedelta(0) + if parsed_result.group("weeks") is not None: + offset += datetime.timedelta(weeks=int(parsed_result.group("weeks"))) + if parsed_result.group("days") is not None: + offset += datetime.timedelta(days=int(parsed_result.group("days"))) + if parsed_result.group("hours") is not None: + offset += datetime.timedelta(hours=int(parsed_result.group("hours"))) + if parsed_result.group("minutes") is not None: + offset += datetime.timedelta( + minutes=int(parsed_result.group("minutes"))) + if parsed_result.group("seconds") is not None: + offset += datetime.timedelta( + seconds=int(parsed_result.group("seconds"))) + + if parsed_result.group("prefix") == "+": + return datetime.datetime.utcnow() + offset + else: + return datetime.datetime.utcnow() - offset + + +def get_relative_time_option(input_string, input_name, backend='cryptography'): + """Return an absolute timespec if a relative timespec or an ASN1 formatted + string is provided. + + The return value will be a datetime object for the cryptography backend, + and a ASN1 formatted string for the pyopenssl backend.""" + result = to_native(input_string) + if result is None: + raise OpenSSLObjectError( + 'The timespec "%s" for %s is not valid' % + input_string, input_name) + # Relative time + if result.startswith("+") or result.startswith("-"): + result_datetime = convert_relative_to_datetime(result) + if backend == 'pyopenssl': + return result_datetime.strftime("%Y%m%d%H%M%SZ") + elif backend == 'cryptography': + return result_datetime + # Absolute time + if backend == 'pyopenssl': + return input_string + elif backend == 'cryptography': + for date_fmt in ['%Y%m%d%H%M%SZ', '%Y%m%d%H%MZ', '%Y%m%d%H%M%S%z', '%Y%m%d%H%M%z']: + try: + return datetime.datetime.strptime(result, date_fmt) + except ValueError: + pass + + raise OpenSSLObjectError( + 'The time spec "%s" for %s is invalid' % + (input_string, input_name) + ) + + +def select_message_digest(digest_string): + digest = None + if digest_string == 'sha256': + digest = hashes.SHA256() + elif digest_string == 'sha384': + digest = hashes.SHA384() + elif digest_string == 'sha512': + digest = hashes.SHA512() + elif digest_string == 'sha1': + digest = hashes.SHA1() + elif digest_string == 'md5': + digest = hashes.MD5() + return digest + + +@six.add_metaclass(abc.ABCMeta) +class OpenSSLObject(object): + + def __init__(self, path, state, force, check_mode): + self.path = path + self.state = state + self.force = force + self.name = os.path.basename(path) + self.changed = False + self.check_mode = check_mode + + def check(self, module, perms_required=True): + """Ensure the resource is in its desired state.""" + + def _check_state(): + return os.path.exists(self.path) + + def _check_perms(module): + file_args = module.load_file_common_arguments(module.params) + return not module.set_fs_attributes_if_different(file_args, False) + + if not perms_required: + return _check_state() + + return _check_state() and _check_perms(module) + + @abc.abstractmethod + def dump(self): + """Serialize the object into a dictionary.""" + + pass + + @abc.abstractmethod + def generate(self): + """Generate the resource.""" + + pass + + def remove(self, module): + """Remove the resource from the filesystem.""" + + try: + os.remove(self.path) + self.changed = True + except OSError as exc: + if exc.errno != errno.ENOENT: + raise OpenSSLObjectError(exc) + else: + pass diff --git a/plugins/module_utils/io.py b/plugins/module_utils/io.py new file mode 100644 index 00000000..debc499a --- /dev/null +++ b/plugins/module_utils/io.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# +# (c) 2016, Yanis Guenane +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +import errno +import os +import tempfile + + +def load_file_if_exists(path, module=None, ignore_errors=False): + ''' + Load the file as a bytes string. If the file does not exist, ``None`` is returned. + + If ``ignore_errors`` is ``True``, will ignore errors. Otherwise, errors are + raised as exceptions if ``module`` is not specified, and result in ``module.fail_json`` + being called when ``module`` is specified. + ''' + try: + with open(path, 'rb') as f: + return f.read() + except EnvironmentError as exc: + if exc.errno == errno.ENOENT: + return None + if ignore_errors: + return None + if module is None: + raise + module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) + except Exception as exc: + if ignore_errors: + return None + if module is None: + raise + module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) + + +def write_file(module, content, default_mode=None, path=None): + ''' + Writes content into destination file as securely as possible. + Uses file arguments from module. + ''' + # Find out parameters for file + try: + file_args = module.load_file_common_arguments(module.params, path=path) + except TypeError: + # The path argument is only supported in Ansible 2.10+. Fall back to + # pre-2.10 behavior of module_utils/crypto.py for older Ansible versions. + file_args = module.load_file_common_arguments(module.params) + if path is not None: + file_args['path'] = path + if file_args['mode'] is None: + file_args['mode'] = default_mode + # Create tempfile name + tmp_fd, tmp_name = tempfile.mkstemp(prefix=b'.ansible_tmp') + try: + os.close(tmp_fd) + except Exception: + pass + module.add_cleanup_file(tmp_name) # if we fail, let Ansible try to remove the file + try: + try: + # Create tempfile + file = os.open(tmp_name, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + os.write(file, content) + os.close(file) + except Exception as e: + try: + os.remove(tmp_name) + except Exception: + pass + module.fail_json(msg='Error while writing result into temporary file: {0}'.format(e)) + # Update destination to wanted permissions + if os.path.exists(file_args['path']): + module.set_fs_attributes_if_different(file_args, False) + # Move tempfile to final destination + module.atomic_move(tmp_name, file_args['path']) + # Try to update permissions again + module.set_fs_attributes_if_different(file_args, False) + except Exception as e: + try: + os.remove(tmp_name) + except Exception: + pass + module.fail_json(msg='Error while writing result: {0}'.format(e)) diff --git a/plugins/modules/__init__.py b/plugins/modules/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/plugins/modules/acme_certificate.py b/plugins/modules/acme_certificate.py index e4a8288e..801ab92f 100644 --- a/plugins/modules/acme_certificate.py +++ b/plugins/modules/acme_certificate.py @@ -505,7 +505,14 @@ from datetime import datetime from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_bytes, to_native -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + parse_name_field, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_name_to_oid, +) + from ansible_collections.community.crypto.plugins.module_utils.acme import ( ModuleFailException, write_file, @@ -1004,8 +1011,8 @@ class ACMEClient(object): x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography.hazmat.backends.default_backend()) matches = True if criterium['subject']: - for k, v in crypto_utils.parse_name_field(criterium['subject']): - oid = crypto_utils.cryptography_name_to_oid(k) + for k, v in parse_name_field(criterium['subject']): + oid = cryptography_name_to_oid(k) value = to_native(v) found = False for attribute in x509.subject: @@ -1016,8 +1023,8 @@ class ACMEClient(object): matches = False break if criterium['issuer']: - for k, v in crypto_utils.parse_name_field(criterium['issuer']): - oid = crypto_utils.cryptography_name_to_oid(k) + for k, v in parse_name_field(criterium['issuer']): + oid = cryptography_name_to_oid(k) value = to_native(v) found = False for attribute in x509.issuer: diff --git a/plugins/modules/ecs_certificate.py b/plugins/modules/ecs_certificate.py index 015936f5..7fe9cc31 100644 --- a/plugins/modules/ecs_certificate.py +++ b/plugins/modules/ecs_certificate.py @@ -518,7 +518,6 @@ from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ( ) import datetime -import json import os import re import time @@ -529,7 +528,13 @@ from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native, to_bytes -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.module_utils.io import ( + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_certificate, +) CRYPTOGRAPHY_IMP_ERR = None try: @@ -602,7 +607,7 @@ class EcsCertificate(object): self.ecs_client = None if self.path and os.path.exists(self.path): try: - self.cert = crypto_utils.load_certificate(self.path, backend='cryptography') + self.cert = load_certificate(self.path, backend='cryptography') except Exception as dummy: self.cert = None # Instantiate the ECS client and then try a no-op connection to verify credentials are valid @@ -774,20 +779,20 @@ class EcsCertificate(object): if self.request_type != 'validate_only': if self.backup: self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, to_bytes(self.cert_details.get('endEntityCert'))) + write_file(module, to_bytes(self.cert_details.get('endEntityCert'))) if self.full_chain_path and self.cert_details.get('chainCerts'): if self.backup: self.backup_full_chain_file = module.backup_local(self.full_chain_path) chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n' - crypto_utils.write_file(module, to_bytes(chain_string), path=self.full_chain_path) + write_file(module, to_bytes(chain_string), path=self.full_chain_path) self.changed = True # If there is no certificate present in path but a tracking ID was specified, save it to disk elif not os.path.exists(self.path) and self.tracking_id: if not module.check_mode: - crypto_utils.write_file(module, to_bytes(self.cert_details.get('endEntityCert'))) + write_file(module, to_bytes(self.cert_details.get('endEntityCert'))) if self.full_chain_path and self.cert_details.get('chainCerts'): chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n' - crypto_utils.write_file(module, to_bytes(chain_string), path=self.full_chain_path) + write_file(module, to_bytes(chain_string), path=self.full_chain_path) self.changed = True def dump(self): diff --git a/plugins/modules/get_certificate.py b/plugins/modules/get_certificate.py index d70c26f6..355c3e22 100644 --- a/plugins/modules/get_certificate.py +++ b/plugins/modules/get_certificate.py @@ -163,7 +163,10 @@ from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_OP from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_bytes -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_oid_to_name, + cryptography_get_extensions_from_cert, +) MINIMAL_PYOPENSSL_VERSION = '0.15' MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' @@ -330,28 +333,28 @@ def main(): x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography_backend()) result['subject'] = {} for attribute in x509.subject: - result['subject'][crypto_utils.cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value + result['subject'][cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value result['expired'] = x509.not_valid_after < datetime.datetime.utcnow() result['extensions'] = [] - for dotted_number, entry in crypto_utils.cryptography_get_extensions_from_cert(x509).items(): + for dotted_number, entry in cryptography_get_extensions_from_cert(x509).items(): oid = cryptography.x509.oid.ObjectIdentifier(dotted_number) result['extensions'].append({ 'critical': entry['critical'], 'asn1_data': base64.b64decode(entry['value']), - 'name': crypto_utils.cryptography_oid_to_name(oid, short=True), + 'name': cryptography_oid_to_name(oid, short=True), }) result['issuer'] = {} for attribute in x509.issuer: - result['issuer'][crypto_utils.cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value + result['issuer'][cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value result['not_after'] = x509.not_valid_after.strftime('%Y%m%d%H%M%SZ') result['not_before'] = x509.not_valid_before.strftime('%Y%m%d%H%M%SZ') result['serial_number'] = x509.serial_number - result['signature_algorithm'] = crypto_utils.cryptography_oid_to_name(x509.signature_algorithm_oid) + result['signature_algorithm'] = cryptography_oid_to_name(x509.signature_algorithm_oid) # We need the -1 offset to get the same values as pyOpenSSL if x509.version == cryptography.x509.Version.v1: diff --git a/plugins/modules/openssh_cert.py b/plugins/modules/openssh_cert.py index 2f740407..2f8b0c57 100644 --- a/plugins/modules/openssh_cert.py +++ b/plugins/modules/openssh_cert.py @@ -208,7 +208,7 @@ from shutil import copy2, rmtree from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native -from ansible_collections.community.crypto.plugins.module_utils.crypto import convert_relative_to_datetime +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import convert_relative_to_datetime class CertificateError(Exception): diff --git a/plugins/modules/openssl_csr.py b/plugins/modules/openssl_csr.py index cbad60ec..6b27bccc 100644 --- a/plugins/modules/openssl_csr.py +++ b/plugins/modules/openssl_csr.py @@ -425,9 +425,33 @@ from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native, to_bytes, to_text -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_privatekey, + load_certificate_request, + parse_name_field, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_get_basic_constraints, + cryptography_get_name, + cryptography_name_to_oid, + cryptography_key_needs_digest_for_signing, + cryptography_parse_key_usage_params, +) + MINIMAL_PYOPENSSL_VERSION = '0.15' MINIMAL_CRYPTOGRAPHY_VERSION = '1.3' @@ -469,11 +493,11 @@ else: CRYPTOGRAPHY_MUST_STAPLE_VALUE = b"\x30\x03\x02\x01\x05" -class CertificateSigningRequestError(crypto_utils.OpenSSLObjectError): +class CertificateSigningRequestError(OpenSSLObjectError): pass -class CertificateSigningRequestBase(crypto_utils.OpenSSLObject): +class CertificateSigningRequestBase(OpenSSLObject): def __init__(self, module): super(CertificateSigningRequestBase, self).__init__( @@ -526,7 +550,7 @@ class CertificateSigningRequestBase(crypto_utils.OpenSSLObject): ] if module.params['subject']: - self.subject = self.subject + crypto_utils.parse_name_field(module.params['subject']) + self.subject = self.subject + parse_name_field(module.params['subject']) self.subject = [(entry[0], entry[1]) for entry in self.subject if entry[1]] if not self.subjectAltName and module.params['use_common_name_for_san']: @@ -559,7 +583,7 @@ class CertificateSigningRequestBase(crypto_utils.OpenSSLObject): self.backup_file = module.backup_local(self.path) if self.return_content: self.csr_bytes = result - crypto_utils.write_file(module, result) + write_file(module, result) self.changed = True file_args = module.load_file_common_arguments(module.params) @@ -608,7 +632,7 @@ class CertificateSigningRequestBase(crypto_utils.OpenSSLObject): result['backup_file'] = self.backup_file if self.return_content: if self.csr_bytes is None: - self.csr_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + self.csr_bytes = load_file_if_exists(self.path, ignore_errors=True) result['csr'] = self.csr_bytes.decode('utf-8') if self.csr_bytes else None return result @@ -676,12 +700,12 @@ class CertificateSigningRequestPyOpenSSL(CertificateSigningRequestBase): def _load_private_key(self): try: - self.privatekey = crypto_utils.load_privatekey( + self.privatekey = load_privatekey( path=self.privatekey_path, content=self.privatekey_content, passphrase=self.privatekey_passphrase ) - except crypto_utils.OpenSSLBadPassphraseError as exc: + except OpenSSLBadPassphraseError as exc: raise CertificateSigningRequestError(exc) def _normalize_san(self, san): @@ -771,7 +795,7 @@ class CertificateSigningRequestPyOpenSSL(CertificateSigningRequestBase): return False try: - csr = crypto_utils.load_certificate_request(self.path, backend='pyopenssl') + csr = load_certificate_request(self.path, backend='pyopenssl') except Exception as dummy: return False @@ -791,27 +815,27 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase): csr = cryptography.x509.CertificateSigningRequestBuilder() try: csr = csr.subject_name(cryptography.x509.Name([ - cryptography.x509.NameAttribute(crypto_utils.cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject + cryptography.x509.NameAttribute(cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject ])) except ValueError as e: raise CertificateSigningRequestError(e) if self.subjectAltName: csr = csr.add_extension(cryptography.x509.SubjectAlternativeName([ - crypto_utils.cryptography_get_name(name) for name in self.subjectAltName + cryptography_get_name(name) for name in self.subjectAltName ]), critical=self.subjectAltName_critical) if self.keyUsage: - params = crypto_utils.cryptography_parse_key_usage_params(self.keyUsage) + params = cryptography_parse_key_usage_params(self.keyUsage) csr = csr.add_extension(cryptography.x509.KeyUsage(**params), critical=self.keyUsage_critical) if self.extendedKeyUsage: - usages = [crypto_utils.cryptography_name_to_oid(usage) for usage in self.extendedKeyUsage] + usages = [cryptography_name_to_oid(usage) for usage in self.extendedKeyUsage] csr = csr.add_extension(cryptography.x509.ExtendedKeyUsage(usages), critical=self.extendedKeyUsage_critical) if self.basicConstraints: params = {} - ca, path_length = crypto_utils.cryptography_get_basic_constraints(self.basicConstraints) + ca, path_length = cryptography_get_basic_constraints(self.basicConstraints) csr = csr.add_extension(cryptography.x509.BasicConstraints(ca, path_length), critical=self.basicConstraints_critical) if self.ocspMustStaple: @@ -835,14 +859,14 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase): if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None: issuers = None if self.authority_cert_issuer is not None: - issuers = [crypto_utils.cryptography_get_name(n) for n in self.authority_cert_issuer] + issuers = [cryptography_get_name(n) for n in self.authority_cert_issuer] csr = csr.add_extension( cryptography.x509.AuthorityKeyIdentifier(self.authority_key_identifier, issuers, self.authority_cert_serial_number), critical=False ) digest = None - if crypto_utils.cryptography_key_needs_digest_for_signing(self.privatekey): + if cryptography_key_needs_digest_for_signing(self.privatekey): if self.digest == 'sha256': digest = cryptography.hazmat.primitives.hashes.SHA256() elif self.digest == 'sha384': @@ -882,7 +906,7 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase): def _check_csr(self): def _check_subject(csr): - subject = [(crypto_utils.cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.subject] + subject = [(cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.subject] current_subject = [(sub.oid, sub.value) for sub in csr.subject] return set(subject) == set(current_subject) @@ -895,7 +919,7 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase): def _check_subjectAltName(extensions): current_altnames_ext = _find_extension(extensions, cryptography.x509.SubjectAlternativeName) current_altnames = [str(altname) for altname in current_altnames_ext.value] if current_altnames_ext else [] - altnames = [str(crypto_utils.cryptography_get_name(altname)) for altname in self.subjectAltName] if self.subjectAltName else [] + altnames = [str(cryptography_get_name(altname)) for altname in self.subjectAltName] if self.subjectAltName else [] if set(altnames) != set(current_altnames): return False if altnames: @@ -909,7 +933,7 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase): return current_keyusage_ext is None elif current_keyusage_ext is None: return False - params = crypto_utils.cryptography_parse_key_usage_params(self.keyUsage) + params = cryptography_parse_key_usage_params(self.keyUsage) for param in params: if getattr(current_keyusage_ext.value, '_' + param) != params[param]: return False @@ -920,7 +944,7 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase): def _check_extenededKeyUsage(extensions): current_usages_ext = _find_extension(extensions, cryptography.x509.ExtendedKeyUsage) current_usages = [str(usage) for usage in current_usages_ext.value] if current_usages_ext else [] - usages = [str(crypto_utils.cryptography_name_to_oid(usage)) for usage in self.extendedKeyUsage] if self.extendedKeyUsage else [] + usages = [str(cryptography_name_to_oid(usage)) for usage in self.extendedKeyUsage] if self.extendedKeyUsage else [] if set(current_usages) != set(usages): return False if usages: @@ -932,7 +956,7 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase): bc_ext = _find_extension(extensions, cryptography.x509.BasicConstraints) current_ca = bc_ext.value.ca if bc_ext else False current_path_length = bc_ext.value.path_length if bc_ext else None - ca, path_length = crypto_utils.cryptography_get_basic_constraints(self.basicConstraints) + ca, path_length = cryptography_get_basic_constraints(self.basicConstraints) # Check CA flag if ca != current_ca: return False @@ -987,7 +1011,7 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase): aci = None csr_aci = None if self.authority_cert_issuer is not None: - aci = [str(crypto_utils.cryptography_get_name(n)) for n in self.authority_cert_issuer] + aci = [str(cryptography_get_name(n)) for n in self.authority_cert_issuer] if ext.value.authority_cert_issuer is not None: csr_aci = [str(n) for n in ext.value.authority_cert_issuer] return (ext.value.key_identifier == self.authority_key_identifier @@ -1019,7 +1043,7 @@ class CertificateSigningRequestCryptography(CertificateSigningRequestBase): return key_a == key_b try: - csr = crypto_utils.load_certificate_request(self.path, backend='cryptography') + csr = load_certificate_request(self.path, backend='cryptography') except Exception as dummy: return False @@ -1136,7 +1160,7 @@ def main(): result = csr.dump() module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: + except OpenSSLObjectError as exc: module.fail_json(msg=to_native(exc)) diff --git a/plugins/modules/openssl_csr_info.py b/plugins/modules/openssl_csr_info.py index 97fca7a4..c644aedf 100644 --- a/plugins/modules/openssl_csr_info.py +++ b/plugins/modules/openssl_csr_info.py @@ -213,9 +213,29 @@ from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native, to_text, to_bytes -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_certificate_request, + get_fingerprint_of_bytes, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_decode_name, + cryptography_get_extensions_from_csr, + cryptography_oid_to_name, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import ( + pyopenssl_normalize_name, + pyopenssl_get_extensions_from_csr, +) + MINIMAL_CRYPTOGRAPHY_VERSION = '1.3' MINIMAL_PYOPENSSL_VERSION = '0.15' @@ -254,7 +274,7 @@ else: TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" -class CertificateSigningRequestInfo(crypto_utils.OpenSSLObject): +class CertificateSigningRequestInfo(OpenSSLObject): def __init__(self, module, backend): super(CertificateSigningRequestInfo, self).__init__( module.params['path'] or '', @@ -269,11 +289,11 @@ class CertificateSigningRequestInfo(crypto_utils.OpenSSLObject): self.content = self.content.encode('utf-8') def generate(self): - # Empty method because crypto_utils.OpenSSLObject wants this + # Empty method because OpenSSLObject wants this pass def dump(self): - # Empty method because crypto_utils.OpenSSLObject wants this + # Empty method because OpenSSLObject wants this pass @abc.abstractmethod @@ -322,7 +342,7 @@ class CertificateSigningRequestInfo(crypto_utils.OpenSSLObject): def get_info(self): result = dict() - self.csr = crypto_utils.load_certificate_request(self.path, content=self.content, backend=self.backend) + self.csr = load_certificate_request(self.path, content=self.content, backend=self.backend) subject = self._get_subject_ordered() result['subject'] = dict() @@ -337,7 +357,7 @@ class CertificateSigningRequestInfo(crypto_utils.OpenSSLObject): result['public_key'] = self._get_public_key(binary=False) pk = self._get_public_key(binary=True) - result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict() + result['public_key_fingerprints'] = get_fingerprint_of_bytes(pk) if pk is not None else dict() if self.backend != 'pyopenssl': ski = self._get_subject_key_identifier() @@ -373,7 +393,7 @@ class CertificateSigningRequestInfoCryptography(CertificateSigningRequestInfo): def _get_subject_ordered(self): result = [] for attribute in self.csr.subject: - result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) + result.append([cryptography_oid_to_name(attribute.oid), attribute.value]) return result def _get_key_usage(self): @@ -418,7 +438,7 @@ class CertificateSigningRequestInfoCryptography(CertificateSigningRequestInfo): try: ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.ExtendedKeyUsage) return sorted([ - crypto_utils.cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value + cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value ]), ext_keyusage_ext.critical except cryptography.x509.ExtensionNotFound: return None, False @@ -452,7 +472,7 @@ class CertificateSigningRequestInfoCryptography(CertificateSigningRequestInfo): def _get_subject_alt_name(self): try: san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName) - result = [crypto_utils.cryptography_decode_name(san) for san in san_ext.value] + result = [cryptography_decode_name(san) for san in san_ext.value] return result, san_ext.critical except cryptography.x509.ExtensionNotFound: return None, False @@ -475,13 +495,13 @@ class CertificateSigningRequestInfoCryptography(CertificateSigningRequestInfo): ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) issuer = None if ext.value.authority_cert_issuer is not None: - issuer = [crypto_utils.cryptography_decode_name(san) for san in ext.value.authority_cert_issuer] + issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer] return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number except cryptography.x509.ExtensionNotFound: return None, None, None def _get_all_extensions(self): - return crypto_utils.cryptography_get_extensions_from_csr(self.csr) + return cryptography_get_extensions_from_csr(self.csr) def _is_signature_valid(self): return self.csr.is_signature_valid @@ -496,7 +516,7 @@ class CertificateSigningRequestInfoPyOpenSSL(CertificateSigningRequestInfo): def __get_name(self, name): result = [] for sub in name.get_components(): - result.append([crypto_utils.pyopenssl_normalize_name(sub[0]), to_text(sub[1])]) + result.append([pyopenssl_normalize_name(sub[0]), to_text(sub[1])]) return result def _get_subject_ordered(self): @@ -506,7 +526,7 @@ class CertificateSigningRequestInfoPyOpenSSL(CertificateSigningRequestInfo): for extension in self.csr.get_extensions(): if extension.get_short_name() == short_name: result = [ - crypto_utils.pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',') + pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',') ] return sorted(result), bool(extension.get_critical()) return None, False @@ -581,7 +601,7 @@ class CertificateSigningRequestInfoPyOpenSSL(CertificateSigningRequestInfo): return None, None, None def _get_all_extensions(self): - return crypto_utils.pyopenssl_get_extensions_from_csr(self.csr) + return pyopenssl_get_extensions_from_csr(self.csr) def _is_signature_valid(self): try: @@ -654,7 +674,7 @@ def main(): result = certificate.get_info() module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: + except OpenSSLObjectError as exc: module.fail_json(msg=to_native(exc)) diff --git a/plugins/modules/openssl_dhparam.py b/plugins/modules/openssl_dhparam.py index 52d66a9d..ed521785 100644 --- a/plugins/modules/openssl_dhparam.py +++ b/plugins/modules/openssl_dhparam.py @@ -131,8 +131,14 @@ from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) +from ansible_collections.community.crypto.plugins.module_utils.crypto.math import ( + count_bits, +) MINIMAL_CRYPTOGRAPHY_VERSION = '2.0' @@ -228,7 +234,7 @@ class DHParameterBase(object): if self.backup_file: result['backup_file'] = self.backup_file if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + content = load_file_if_exists(self.path, ignore_errors=True) result['dhparams'] = content.decode('utf-8') if content else None return result @@ -317,7 +323,7 @@ class DHParameterCryptography(DHParameterBase): # Write result if self.backup: self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, result) + write_file(module, result) def _check_params_valid(self, module): """Check if the params are in the correct state""" @@ -329,7 +335,7 @@ class DHParameterCryptography(DHParameterBase): except Exception as dummy: return False # Check parameters - bits = crypto_utils.count_bits(params.parameter_numbers().p) + bits = count_bits(params.parameter_numbers().p) return bits == self.size diff --git a/plugins/modules/openssl_pkcs12.py b/plugins/modules/openssl_pkcs12.py index 6a6bc358..3791de2a 100644 --- a/plugins/modules/openssl_pkcs12.py +++ b/plugins/modules/openssl_pkcs12.py @@ -186,7 +186,21 @@ import traceback from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_bytes, to_native -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_privatekey, + load_certificate, +) PYOPENSSL_IMP_ERR = None try: @@ -198,11 +212,11 @@ else: pyopenssl_found = True -class PkcsError(crypto_utils.OpenSSLObjectError): +class PkcsError(OpenSSLObjectError): pass -class Pkcs(crypto_utils.OpenSSLObject): +class Pkcs(OpenSSLObject): def __init__(self, module): super(Pkcs, self).__init__( @@ -239,11 +253,10 @@ class Pkcs(crypto_utils.OpenSSLObject): def _check_pkey_passphrase(): if self.privatekey_passphrase: try: - crypto_utils.load_privatekey(self.privatekey_path, - self.privatekey_passphrase) + load_privatekey(self.privatekey_path, self.privatekey_passphrase) except crypto.Error: return False - except crypto_utils.OpenSSLBadPassphraseError: + except OpenSSLBadPassphraseError: return False return True @@ -307,7 +320,7 @@ class Pkcs(crypto_utils.OpenSSLObject): result['backup_file'] = self.backup_file if self.return_content: if self.pkcs12_bytes is None: - self.pkcs12_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + self.pkcs12_bytes = load_file_if_exists(self.path, ignore_errors=True) result['pkcs12'] = base64.b64encode(self.pkcs12_bytes) if self.pkcs12_bytes else None return result @@ -317,24 +330,20 @@ class Pkcs(crypto_utils.OpenSSLObject): self.pkcs12 = crypto.PKCS12() if self.other_certificates: - other_certs = [crypto_utils.load_certificate(other_cert) for other_cert + other_certs = [load_certificate(other_cert) for other_cert in self.other_certificates] self.pkcs12.set_ca_certificates(other_certs) if self.certificate_path: - self.pkcs12.set_certificate(crypto_utils.load_certificate( - self.certificate_path)) + self.pkcs12.set_certificate(load_certificate(self.certificate_path)) if self.friendly_name: self.pkcs12.set_friendlyname(to_bytes(self.friendly_name)) if self.privatekey_path: try: - self.pkcs12.set_privatekey(crypto_utils.load_privatekey( - self.privatekey_path, - self.privatekey_passphrase) - ) - except crypto_utils.OpenSSLBadPassphraseError as exc: + self.pkcs12.set_privatekey(load_privatekey(self.privatekey_path, self.privatekey_passphrase)) + except OpenSSLBadPassphraseError as exc: raise PkcsError(exc) return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size) @@ -372,7 +381,7 @@ class Pkcs(crypto_utils.OpenSSLObject): """Write the PKCS#12 file.""" if self.backup: self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, content, mode) + write_file(module, content, mode) if self.return_content: self.pkcs12_bytes = content @@ -459,7 +468,7 @@ def main(): result['mode'] = file_mode module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: + except OpenSSLObjectError as exc: module.fail_json(msg=to_native(exc)) diff --git a/plugins/modules/openssl_privatekey.py b/plugins/modules/openssl_privatekey.py index 45b92a1d..6a9117e9 100644 --- a/plugins/modules/openssl_privatekey.py +++ b/plugins/modules/openssl_privatekey.py @@ -281,7 +281,31 @@ from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native, to_bytes -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + CRYPTOGRAPHY_HAS_X25519, + CRYPTOGRAPHY_HAS_X25519_FULL, + CRYPTOGRAPHY_HAS_X448, + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED448, + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_privatekey, + get_fingerprint, + get_fingerprint_of_bytes, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.identify import ( + identify_private_key_format, +) MINIMAL_PYOPENSSL_VERSION = '0.6' MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' @@ -314,20 +338,12 @@ except ImportError: else: CRYPTOGRAPHY_FOUND = True -from ansible_collections.community.crypto.plugins.module_utils.crypto import ( - CRYPTOGRAPHY_HAS_X25519, - CRYPTOGRAPHY_HAS_X25519_FULL, - CRYPTOGRAPHY_HAS_X448, - CRYPTOGRAPHY_HAS_ED25519, - CRYPTOGRAPHY_HAS_ED448, -) - -class PrivateKeyError(crypto_utils.OpenSSLObjectError): +class PrivateKeyError(OpenSSLObjectError): pass -class PrivateKeyBase(crypto_utils.OpenSSLObject): +class PrivateKeyBase(OpenSSLObject): def __init__(self, module): super(PrivateKeyBase, self).__init__( @@ -385,7 +401,7 @@ class PrivateKeyBase(crypto_utils.OpenSSLObject): privatekey_data = self._get_private_key_data() if self.return_content: self.privatekey_bytes = privatekey_data - crypto_utils.write_file(module, privatekey_data, 0o600) + write_file(module, privatekey_data, 0o600) self.changed = True elif not self.check(module, perms_required=False, ignore_conversion=False): # Convert @@ -395,7 +411,7 @@ class PrivateKeyBase(crypto_utils.OpenSSLObject): privatekey_data = self._get_private_key_data() if self.return_content: self.privatekey_bytes = privatekey_data - crypto_utils.write_file(module, privatekey_data, 0o600) + write_file(module, privatekey_data, 0o600) self.changed = True self.fingerprint = self._get_fingerprint() @@ -473,9 +489,9 @@ class PrivateKeyBase(crypto_utils.OpenSSLObject): result['backup_file'] = self.backup_file if self.return_content: if self.privatekey_bytes is None: - self.privatekey_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + self.privatekey_bytes = load_file_if_exists(self.path, ignore_errors=True) if self.privatekey_bytes: - if crypto_utils.identify_private_key_format(self.privatekey_bytes) == 'raw': + if identify_private_key_format(self.privatekey_bytes) == 'raw': result['privatekey'] = base64.b64encode(self.privatekey_bytes) else: result['privatekey'] = self.privatekey_bytes.decode('utf-8') @@ -513,8 +529,8 @@ class PrivateKeyPyOpenSSL(PrivateKeyBase): """Make sure that the private key has been loaded.""" if self.privatekey is None: try: - self.privatekey = privatekey = crypto_utils.load_privatekey(self.path, self.passphrase) - except crypto_utils.OpenSSLBadPassphraseError as exc: + self.privatekey = privatekey = load_privatekey(self.path, self.passphrase) + except OpenSSLBadPassphraseError as exc: raise PrivateKeyError(exc) def _get_private_key_data(self): @@ -526,11 +542,11 @@ class PrivateKeyPyOpenSSL(PrivateKeyBase): return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey) def _get_fingerprint(self): - return crypto_utils.get_fingerprint(self.path, self.passphrase) + return get_fingerprint(self.path, self.passphrase) def _check_passphrase(self): try: - crypto_utils.load_privatekey(self.path, self.passphrase) + load_privatekey(self.path, self.passphrase) return True except Exception as dummy: return False @@ -719,7 +735,7 @@ class PrivateKeyCryptography(PrivateKeyBase): with open(self.path, 'rb') as f: data = f.read() # Interpret bytes depending on format. - format = crypto_utils.identify_private_key_format(data) + format = identify_private_key_format(data) if format == 'raw': if len(data) == 56 and CRYPTOGRAPHY_HAS_X448: return cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data) @@ -754,13 +770,13 @@ class PrivateKeyCryptography(PrivateKeyBase): cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo ) # Get fingerprints of public_key_bytes - return crypto_utils.get_fingerprint_of_bytes(public_key_bytes) + return get_fingerprint_of_bytes(public_key_bytes) def _check_passphrase(self): try: with open(self.path, 'rb') as f: data = f.read() - format = crypto_utils.identify_private_key_format(data) + format = identify_private_key_format(data) if format == 'raw': # Raw keys cannot be encrypted. To avoid incompatibilities, we try to # actually load the key (and return False when this fails). @@ -807,7 +823,7 @@ class PrivateKeyCryptography(PrivateKeyBase): try: with open(self.path, 'rb') as f: content = f.read() - format = crypto_utils.identify_private_key_format(content) + format = identify_private_key_format(content) return format == self._get_wanted_format() except Exception as dummy: return False @@ -926,7 +942,7 @@ def main(): result = private_key.dump() module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: + except OpenSSLObjectError as exc: module.fail_json(msg=to_native(exc)) diff --git a/plugins/modules/openssl_privatekey_info.py b/plugins/modules/openssl_privatekey_info.py index 43fec42a..40e1a0a9 100644 --- a/plugins/modules/openssl_privatekey_info.py +++ b/plugins/modules/openssl_privatekey_info.py @@ -145,7 +145,25 @@ from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native, to_bytes -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + CRYPTOGRAPHY_HAS_X25519, + CRYPTOGRAPHY_HAS_X448, + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED448, + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_privatekey, + get_fingerprint_of_bytes, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.math import ( + binary_exp_mod, + quick_is_not_prime, +) + MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' MINIMAL_PYOPENSSL_VERSION = '0.15' @@ -166,26 +184,6 @@ try: import cryptography from cryptography.hazmat.primitives import serialization CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) - try: - import cryptography.hazmat.primitives.asymmetric.x25519 - CRYPTOGRAPHY_HAS_X25519 = True - except ImportError: - CRYPTOGRAPHY_HAS_X25519 = False - try: - import cryptography.hazmat.primitives.asymmetric.x448 - CRYPTOGRAPHY_HAS_X448 = True - except ImportError: - CRYPTOGRAPHY_HAS_X448 = False - try: - import cryptography.hazmat.primitives.asymmetric.ed25519 - CRYPTOGRAPHY_HAS_ED25519 = True - except ImportError: - CRYPTOGRAPHY_HAS_ED25519 = False - try: - import cryptography.hazmat.primitives.asymmetric.ed448 - CRYPTOGRAPHY_HAS_ED448 = True - except ImportError: - CRYPTOGRAPHY_HAS_ED448 = False except ImportError: CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() CRYPTOGRAPHY_FOUND = False @@ -254,13 +252,13 @@ def _check_dsa_consistency(key_public_data, key_private_data): if (p - 1) % q != 0: return False # Check that g**q mod p == 1 - if crypto_utils.binary_exp_mod(g, q, p) != 1: + if binary_exp_mod(g, q, p) != 1: return False # Check whether g**x mod p == y - if crypto_utils.binary_exp_mod(g, x, p) != y: + if binary_exp_mod(g, x, p) != y: return False # Check (quickly) whether p or q are not primes - if crypto_utils.quick_is_not_prime(q) or crypto_utils.quick_is_not_prime(p): + if quick_is_not_prime(q) or quick_is_not_prime(p): return False return True @@ -320,7 +318,7 @@ def _is_cryptography_key_consistent(key, key_public_data, key_private_data): return None -class PrivateKeyInfo(crypto_utils.OpenSSLObject): +class PrivateKeyInfo(OpenSSLObject): def __init__(self, module, backend): super(PrivateKeyInfo, self).__init__( module.params['path'] or '', @@ -336,11 +334,11 @@ class PrivateKeyInfo(crypto_utils.OpenSSLObject): self.return_private_key_data = module.params['return_private_key_data'] def generate(self): - # Empty method because crypto_utils.OpenSSLObject wants this + # Empty method because OpenSSLObject wants this pass def dump(self): - # Empty method because crypto_utils.OpenSSLObject wants this + # Empty method because OpenSSLObject wants this pass @abc.abstractmethod @@ -372,19 +370,19 @@ class PrivateKeyInfo(crypto_utils.OpenSSLObject): except (IOError, OSError) as exc: self.module.fail_json(msg=to_native(exc), **result) try: - self.key = crypto_utils.load_privatekey( + self.key = load_privatekey( path=None, content=priv_key_detail, passphrase=to_bytes(self.passphrase) if self.passphrase is not None else self.passphrase, backend=self.backend ) result['can_parse_key'] = True - except crypto_utils.OpenSSLObjectError as exc: + except OpenSSLObjectError as exc: self.module.fail_json(msg=to_native(exc), **result) result['public_key'] = self._get_public_key(binary=False) pk = self._get_public_key(binary=True) - result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict() + result['public_key_fingerprints'] = get_fingerprint_of_bytes(pk) if pk is not None else dict() key_type, key_public_data, key_private_data = self._get_key_info() result['type'] = key_type @@ -643,7 +641,7 @@ def main(): result = privatekey.get_info() module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: + except OpenSSLObjectError as exc: module.fail_json(msg=to_native(exc)) diff --git a/plugins/modules/openssl_publickey.py b/plugins/modules/openssl_publickey.py index 30a0c83e..9d4ebf09 100644 --- a/plugins/modules/openssl_publickey.py +++ b/plugins/modules/openssl_publickey.py @@ -184,7 +184,21 @@ from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_privatekey, + get_fingerprint, +) MINIMAL_PYOPENSSL_VERSION = '16.0.0' MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' @@ -214,11 +228,11 @@ else: CRYPTOGRAPHY_FOUND = True -class PublicKeyError(crypto_utils.OpenSSLObjectError): +class PublicKeyError(OpenSSLObjectError): pass -class PublicKey(crypto_utils.OpenSSLObject): +class PublicKey(OpenSSLObject): def __init__(self, module, backend): super(PublicKey, self).__init__( @@ -243,7 +257,7 @@ class PublicKey(crypto_utils.OpenSSLObject): self.backup_file = None def _create_publickey(self, module): - self.privatekey = crypto_utils.load_privatekey( + self.privatekey = load_privatekey( path=self.privatekey_path, content=self.privatekey_content, passphrase=self.privatekey_passphrase, @@ -282,15 +296,15 @@ class PublicKey(crypto_utils.OpenSSLObject): if self.backup: self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, publickey_content) + write_file(module, publickey_content) self.changed = True - except crypto_utils.OpenSSLBadPassphraseError as exc: + except OpenSSLBadPassphraseError as exc: raise PublicKeyError(exc) except (IOError, OSError) as exc: raise PublicKeyError(exc) - self.fingerprint = crypto_utils.get_fingerprint( + self.fingerprint = get_fingerprint( path=self.privatekey_path, content=self.privatekey_content, passphrase=self.privatekey_passphrase, @@ -338,7 +352,7 @@ class PublicKey(crypto_utils.OpenSSLObject): try: desired_publickey = self._create_publickey(module) - except crypto_utils.OpenSSLBadPassphraseError as exc: + except OpenSSLBadPassphraseError as exc: raise PublicKeyError(exc) return publickey_content == desired_publickey @@ -367,7 +381,7 @@ class PublicKey(crypto_utils.OpenSSLObject): result['backup_file'] = self.backup_file if self.return_content: if self.publickey_bytes is None: - self.publickey_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + self.publickey_bytes = load_file_if_exists(self.path, ignore_errors=True) result['publickey'] = self.publickey_bytes.decode('utf-8') if self.publickey_bytes else None return result @@ -464,7 +478,7 @@ def main(): result = public_key.dump() module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: + except OpenSSLObjectError as exc: module.fail_json(msg=to_native(exc)) diff --git a/plugins/modules/x509_certificate.py b/plugins/modules/x509_certificate.py index d112dc63..8d1564f6 100644 --- a/plugins/modules/x509_certificate.py +++ b/plugins/modules/x509_certificate.py @@ -860,10 +860,38 @@ from random import randint from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native, to_bytes, to_text -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ECSClient, RestOperationException, SessionConfigurationException +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file_if_exists, + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_privatekey, + load_certificate, + load_certificate_request, + parse_name_field, + get_relative_time_option, + select_message_digest, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_compare_public_keys, + cryptography_get_name, + cryptography_name_to_oid, + cryptography_key_needs_digest_for_signing, + cryptography_parse_key_usage_params, + cryptography_serial_number_of_cert, +) + MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' MINIMAL_PYOPENSSL_VERSION = '0.15' @@ -894,11 +922,11 @@ else: CRYPTOGRAPHY_FOUND = True -class CertificateError(crypto_utils.OpenSSLObjectError): +class CertificateError(OpenSSLObjectError): pass -class Certificate(crypto_utils.OpenSSLObject): +class Certificate(OpenSSLObject): def __init__(self, module, backend): super(Certificate, self).__init__( @@ -944,7 +972,7 @@ class Certificate(crypto_utils.OpenSSLObject): except OpenSSL.SSL.Error: return False elif self.backend == 'cryptography': - return crypto_utils.cryptography_compare_public_keys(self.cert.public_key(), self.privatekey.public_key()) + return cryptography_compare_public_keys(self.cert.public_key(), self.privatekey.public_key()) def _validate_csr(self): if self.backend == 'pyopenssl': @@ -971,7 +999,7 @@ class Certificate(crypto_utils.OpenSSLObject): # Verify that CSR is signed by certificate's private key if not self.csr.is_signature_valid: return False - if not crypto_utils.cryptography_compare_public_keys(self.csr.public_key(), self.cert.public_key()): + if not cryptography_compare_public_keys(self.csr.public_key(), self.cert.public_key()): return False # Check subject if self.csr.subject != self.cert.subject: @@ -1012,25 +1040,25 @@ class Certificate(crypto_utils.OpenSSLObject): return False try: - self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) + self.cert = load_certificate(self.path, backend=self.backend) except Exception as dummy: return False if self.privatekey_path or self.privatekey_content: try: - self.privatekey = crypto_utils.load_privatekey( + self.privatekey = load_privatekey( path=self.privatekey_path, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend ) - except crypto_utils.OpenSSLBadPassphraseError as exc: + except OpenSSLBadPassphraseError as exc: raise CertificateError(exc) if not self._validate_privatekey(): return False if self.csr_path or self.csr_content: - self.csr = crypto_utils.load_certificate_request( + self.csr = load_certificate_request( path=self.csr_path, content=self.csr_content, backend=self.backend @@ -1093,9 +1121,9 @@ class SelfSignedCertificateCryptography(Certificate): def __init__(self, module): super(SelfSignedCertificateCryptography, self).__init__(module, 'cryptography') self.create_subject_key_identifier = module.params['selfsigned_create_subject_key_identifier'] - self.notBefore = crypto_utils.get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) - self.notAfter = crypto_utils.get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend) - self.digest = crypto_utils.select_message_digest(module.params['selfsigned_digest']) + self.notBefore = get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) + self.notAfter = get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend) + self.digest = select_message_digest(module.params['selfsigned_digest']) self.version = module.params['selfsigned_version'] self.serial_number = x509.random_serial_number() @@ -1108,7 +1136,7 @@ class SelfSignedCertificateCryptography(Certificate): 'The private key file {0} does not exist'.format(self.privatekey_path) ) - self.csr = crypto_utils.load_certificate_request( + self.csr = load_certificate_request( path=self.csr_path, content=self.csr_content, backend=self.backend @@ -1116,16 +1144,16 @@ class SelfSignedCertificateCryptography(Certificate): self._module = module try: - self.privatekey = crypto_utils.load_privatekey( + self.privatekey = load_privatekey( path=self.privatekey_path, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend ) - except crypto_utils.OpenSSLBadPassphraseError as exc: + except OpenSSLBadPassphraseError as exc: module.fail_json(msg=to_native(exc)) - if crypto_utils.cryptography_key_needs_digest_for_signing(self.privatekey): + if cryptography_key_needs_digest_for_signing(self.privatekey): if self.digest is None: raise CertificateError( 'The digest %s is not supported with the cryptography backend' % module.params['selfsigned_digest'] @@ -1180,10 +1208,10 @@ class SelfSignedCertificateCryptography(Certificate): if self.backup: self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, certificate.public_bytes(Encoding.PEM)) + write_file(module, certificate.public_bytes(Encoding.PEM)) self.changed = True else: - self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) + self.cert = load_certificate(self.path, backend=self.backend) file_args = module.load_file_common_arguments(module.params) if module.set_fs_attributes_if_different(file_args, False): @@ -1200,7 +1228,7 @@ class SelfSignedCertificateCryptography(Certificate): if self.backup_file: result['backup_file'] = self.backup_file if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + content = load_file_if_exists(self.path, ignore_errors=True) result['certificate'] = content.decode('utf-8') if content else None if check_mode: @@ -1213,7 +1241,7 @@ class SelfSignedCertificateCryptography(Certificate): result.update({ 'notBefore': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"), 'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"), - 'serial_number': self.cert.serial_number, + 'serial_number': cryptography_serial_number_of_cert(self.cert), }) return result @@ -1226,8 +1254,8 @@ class SelfSignedCertificate(Certificate): super(SelfSignedCertificate, self).__init__(module, 'pyopenssl') if module.params['selfsigned_create_subject_key_identifier'] != 'create_if_not_provided': module.fail_json(msg='selfsigned_create_subject_key_identifier cannot be used with the pyOpenSSL backend!') - self.notBefore = crypto_utils.get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) - self.notAfter = crypto_utils.get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend) + self.notBefore = get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) + self.notAfter = get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend) self.digest = module.params['selfsigned_digest'] self.version = module.params['selfsigned_version'] self.serial_number = randint(1000, 99999) @@ -1241,17 +1269,17 @@ class SelfSignedCertificate(Certificate): 'The private key file {0} does not exist'.format(self.privatekey_path) ) - self.csr = crypto_utils.load_certificate_request( + self.csr = load_certificate_request( path=self.csr_path, content=self.csr_content, ) try: - self.privatekey = crypto_utils.load_privatekey( + self.privatekey = load_privatekey( path=self.privatekey_path, content=self.privatekey_content, passphrase=self.privatekey_passphrase, ) - except crypto_utils.OpenSSLBadPassphraseError as exc: + except OpenSSLBadPassphraseError as exc: module.fail_json(msg=str(exc)) def generate(self, module): @@ -1282,7 +1310,7 @@ class SelfSignedCertificate(Certificate): if self.backup: self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)) + write_file(module, crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)) self.changed = True file_args = module.load_file_common_arguments(module.params) @@ -1300,7 +1328,7 @@ class SelfSignedCertificate(Certificate): if self.backup_file: result['backup_file'] = self.backup_file if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + content = load_file_if_exists(self.path, ignore_errors=True) result['certificate'] = content.decode('utf-8') if content else None if check_mode: @@ -1325,9 +1353,9 @@ class OwnCACertificateCryptography(Certificate): super(OwnCACertificateCryptography, self).__init__(module, 'cryptography') self.create_subject_key_identifier = module.params['ownca_create_subject_key_identifier'] self.create_authority_key_identifier = module.params['ownca_create_authority_key_identifier'] - self.notBefore = crypto_utils.get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) - self.notAfter = crypto_utils.get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) - self.digest = crypto_utils.select_message_digest(module.params['ownca_digest']) + self.notBefore = get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) + self.notAfter = get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) + self.digest = select_message_digest(module.params['ownca_digest']) self.version = module.params['ownca_version'] self.serial_number = x509.random_serial_number() self.ca_cert_path = module.params['ownca_path'] @@ -1353,27 +1381,27 @@ class OwnCACertificateCryptography(Certificate): 'The CA private key file {0} does not exist'.format(self.ca_privatekey_path) ) - self.csr = crypto_utils.load_certificate_request( + self.csr = load_certificate_request( path=self.csr_path, content=self.csr_content, backend=self.backend ) - self.ca_cert = crypto_utils.load_certificate( + self.ca_cert = load_certificate( path=self.ca_cert_path, content=self.ca_cert_content, backend=self.backend ) try: - self.ca_private_key = crypto_utils.load_privatekey( + self.ca_private_key = load_privatekey( path=self.ca_privatekey_path, content=self.ca_privatekey_content, passphrase=self.ca_privatekey_passphrase, backend=self.backend ) - except crypto_utils.OpenSSLBadPassphraseError as exc: + except OpenSSLBadPassphraseError as exc: module.fail_json(msg=str(exc)) - if crypto_utils.cryptography_key_needs_digest_for_signing(self.ca_private_key): + if cryptography_key_needs_digest_for_signing(self.ca_private_key): if self.digest is None: raise CertificateError( 'The digest %s is not supported with the cryptography backend' % module.params['ownca_digest'] @@ -1449,10 +1477,10 @@ class OwnCACertificateCryptography(Certificate): if self.backup: self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, certificate.public_bytes(Encoding.PEM)) + write_file(module, certificate.public_bytes(Encoding.PEM)) self.changed = True else: - self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) + self.cert = load_certificate(self.path, backend=self.backend) file_args = module.load_file_common_arguments(module.params) if module.set_fs_attributes_if_different(file_args, False): @@ -1497,7 +1525,7 @@ class OwnCACertificateCryptography(Certificate): if self.backup_file: result['backup_file'] = self.backup_file if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + content = load_file_if_exists(self.path, ignore_errors=True) result['certificate'] = content.decode('utf-8') if content else None if check_mode: @@ -1510,7 +1538,7 @@ class OwnCACertificateCryptography(Certificate): result.update({ 'notBefore': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"), 'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"), - 'serial_number': self.cert.serial_number, + 'serial_number': cryptography_serial_number_of_cert(self.cert), }) return result @@ -1521,8 +1549,8 @@ class OwnCACertificate(Certificate): def __init__(self, module): super(OwnCACertificate, self).__init__(module, 'pyopenssl') - self.notBefore = crypto_utils.get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) - self.notAfter = crypto_utils.get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) + self.notBefore = get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) + self.notAfter = get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) self.digest = module.params['ownca_digest'] self.version = module.params['ownca_version'] self.serial_number = randint(1000, 99999) @@ -1553,21 +1581,21 @@ class OwnCACertificate(Certificate): 'The CA private key file {0} does not exist'.format(self.ca_privatekey_path) ) - self.csr = crypto_utils.load_certificate_request( + self.csr = load_certificate_request( path=self.csr_path, content=self.csr_content, ) - self.ca_cert = crypto_utils.load_certificate( + self.ca_cert = load_certificate( path=self.ca_cert_path, content=self.ca_cert_content, ) try: - self.ca_privatekey = crypto_utils.load_privatekey( + self.ca_privatekey = load_privatekey( path=self.ca_privatekey_path, content=self.ca_privatekey_content, passphrase=self.ca_privatekey_passphrase ) - except crypto_utils.OpenSSLBadPassphraseError as exc: + except OpenSSLBadPassphraseError as exc: module.fail_json(msg=str(exc)) def generate(self, module): @@ -1603,7 +1631,7 @@ class OwnCACertificate(Certificate): if self.backup: self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)) + write_file(module, crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)) self.changed = True file_args = module.load_file_common_arguments(module.params) @@ -1623,7 +1651,7 @@ class OwnCACertificate(Certificate): if self.backup_file: result['backup_file'] = self.backup_file if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + content = load_file_if_exists(self.path, ignore_errors=True) result['certificate'] = content.decode('utf-8') if content else None if check_mode: @@ -1666,12 +1694,12 @@ class AssertOnlyCertificateBase(Certificate): self.signature_algorithms = module.params['signature_algorithms'] if module.params['subject']: - self.subject = crypto_utils.parse_name_field(module.params['subject']) + self.subject = parse_name_field(module.params['subject']) else: self.subject = [] self.subject_strict = module.params['subject_strict'] if module.params['issuer']: - self.issuer = crypto_utils.parse_name_field(module.params['issuer']) + self.issuer = parse_name_field(module.params['issuer']) else: self.issuer = [] self.issuer_strict = module.params['issuer_strict'] @@ -1696,19 +1724,19 @@ class AssertOnlyCertificateBase(Certificate): self.valid_in = "+" + self.valid_in + "s" # Load objects - self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) + self.cert = load_certificate(self.path, backend=self.backend) if self.privatekey_path is not None or self.privatekey_content is not None: try: - self.privatekey = crypto_utils.load_privatekey( + self.privatekey = load_privatekey( path=self.privatekey_path, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend ) - except crypto_utils.OpenSSLBadPassphraseError as exc: + except OpenSSLBadPassphraseError as exc: raise CertificateError(exc) if self.csr_path is not None or self.csr_content is not None: - self.csr = crypto_utils.load_certificate_request( + self.csr = load_certificate_request( path=self.csr_path, content=self.csr_content, backend=self.backend @@ -1883,7 +1911,7 @@ class AssertOnlyCertificateBase(Certificate): if self.not_before is not None: cert_not_valid_before = self._validate_not_before() - if cert_not_valid_before != crypto_utils.get_relative_time_option(self.not_before, 'not_before', backend=self.backend): + if cert_not_valid_before != get_relative_time_option(self.not_before, 'not_before', backend=self.backend): messages.append( 'Invalid not_before component (got %s, expected %s to be present)' % (cert_not_valid_before, self.not_before) @@ -1891,7 +1919,7 @@ class AssertOnlyCertificateBase(Certificate): if self.not_after is not None: cert_not_valid_after = self._validate_not_after() - if cert_not_valid_after != crypto_utils.get_relative_time_option(self.not_after, 'not_after', backend=self.backend): + if cert_not_valid_after != get_relative_time_option(self.not_after, 'not_after', backend=self.backend): messages.append( 'Invalid not_after component (got %s, expected %s to be present)' % (cert_not_valid_after, self.not_after) @@ -1941,7 +1969,7 @@ class AssertOnlyCertificateBase(Certificate): 'csr': self.csr_path, } if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + content = load_file_if_exists(self.path, ignore_errors=True) result['certificate'] = content.decode('utf-8') if content else None return result @@ -1952,12 +1980,12 @@ class AssertOnlyCertificateCryptography(AssertOnlyCertificateBase): super(AssertOnlyCertificateCryptography, self).__init__(module, 'cryptography') def _validate_privatekey(self): - return crypto_utils.cryptography_compare_public_keys(self.cert.public_key(), self.privatekey.public_key()) + return cryptography_compare_public_keys(self.cert.public_key(), self.privatekey.public_key()) def _validate_csr_signature(self): if not self.csr.is_signature_valid: return False - return crypto_utils.cryptography_compare_public_keys(self.csr.public_key(), self.cert.public_key()) + return cryptography_compare_public_keys(self.csr.public_key(), self.cert.public_key()) def _validate_csr_subject(self): return self.csr.subject == self.cert.subject @@ -1981,14 +2009,14 @@ class AssertOnlyCertificateCryptography(AssertOnlyCertificateBase): return self.cert.signature_algorithm_oid._name def _validate_subject(self): - expected_subject = Name([NameAttribute(oid=crypto_utils.cryptography_name_to_oid(sub[0]), value=to_text(sub[1])) + expected_subject = Name([NameAttribute(oid=cryptography_name_to_oid(sub[0]), value=to_text(sub[1])) for sub in self.subject]) cert_subject = self.cert.subject if not compare_sets(expected_subject, cert_subject, self.subject_strict): return expected_subject, cert_subject def _validate_issuer(self): - expected_issuer = Name([NameAttribute(oid=crypto_utils.cryptography_name_to_oid(iss[0]), value=to_text(iss[1])) + expected_issuer = Name([NameAttribute(oid=cryptography_name_to_oid(iss[0]), value=to_text(iss[1])) for iss in self.issuer]) cert_issuer = self.cert.issuer if not compare_sets(expected_issuer, cert_issuer, self.issuer_strict): @@ -2026,7 +2054,7 @@ class AssertOnlyCertificateCryptography(AssertOnlyCertificateBase): decipher_only=current_key_usage.decipher_only )) - key_usages = crypto_utils.cryptography_parse_key_usage_params(self.key_usage) + key_usages = cryptography_parse_key_usage_params(self.key_usage) if not compare_dicts(key_usages, test_key_usage, self.key_usage_strict): return self.key_usage, [k for k, v in test_key_usage.items() if v is True] @@ -2038,7 +2066,7 @@ class AssertOnlyCertificateCryptography(AssertOnlyCertificateBase): def _validate_extended_key_usage(self): try: current_ext_keyusage = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value - usages = [crypto_utils.cryptography_name_to_oid(usage) for usage in self.extended_key_usage] + usages = [cryptography_name_to_oid(usage) for usage in self.extended_key_usage] expected_ext_keyusage = x509.ExtendedKeyUsage(usages) if not compare_sets(expected_ext_keyusage, current_ext_keyusage, self.extended_key_usage_strict): return [eku.value for eku in expected_ext_keyusage], [eku.value for eku in current_ext_keyusage] @@ -2051,7 +2079,7 @@ class AssertOnlyCertificateCryptography(AssertOnlyCertificateBase): def _validate_subject_alt_name(self): try: current_san = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value - expected_san = [crypto_utils.cryptography_get_name(san) for san in self.subject_alt_name] + expected_san = [cryptography_get_name(san) for san in self.subject_alt_name] if not compare_sets(expected_san, current_san, self.subject_alt_name_strict): return self.subject_alt_name, current_san except cryptography.x509.ExtensionNotFound: @@ -2066,15 +2094,15 @@ class AssertOnlyCertificateCryptography(AssertOnlyCertificateBase): return self.cert.not_valid_after def _validate_valid_at(self): - rt = crypto_utils.get_relative_time_option(self.valid_at, 'valid_at', backend=self.backend) + rt = get_relative_time_option(self.valid_at, 'valid_at', backend=self.backend) return self.cert.not_valid_before, rt, self.cert.not_valid_after def _validate_invalid_at(self): - rt = crypto_utils.get_relative_time_option(self.invalid_at, 'invalid_at', backend=self.backend) + rt = get_relative_time_option(self.invalid_at, 'invalid_at', backend=self.backend) return self.cert.not_valid_before, rt, self.cert.not_valid_after def _validate_valid_in(self): - valid_in_date = crypto_utils.get_relative_time_option(self.valid_in, "valid_in", backend=self.backend) + valid_in_date = get_relative_time_option(self.valid_in, "valid_in", backend=self.backend) return self.cert.not_valid_before, valid_in_date, self.cert.not_valid_after @@ -2231,17 +2259,17 @@ class AssertOnlyCertificate(AssertOnlyCertificateBase): return self.cert.get_notAfter() def _validate_valid_at(self): - rt = crypto_utils.get_relative_time_option(self.valid_at, "valid_at", backend=self.backend) + rt = get_relative_time_option(self.valid_at, "valid_at", backend=self.backend) rt = to_bytes(rt, errors='surrogate_or_strict') return self.cert.get_notBefore(), rt, self.cert.get_notAfter() def _validate_invalid_at(self): - rt = crypto_utils.get_relative_time_option(self.invalid_at, "invalid_at", backend=self.backend) + rt = get_relative_time_option(self.invalid_at, "invalid_at", backend=self.backend) rt = to_bytes(rt, errors='surrogate_or_strict') return self.cert.get_notBefore(), rt, self.cert.get_notAfter() def _validate_valid_in(self): - valid_in_asn1 = crypto_utils.get_relative_time_option(self.valid_in, "valid_in", backend=self.backend) + valid_in_asn1 = get_relative_time_option(self.valid_in, "valid_in", backend=self.backend) valid_in_date = to_bytes(valid_in_asn1, errors='surrogate_or_strict') return self.cert.get_notBefore(), valid_in_date, self.cert.get_notAfter() @@ -2252,14 +2280,14 @@ class EntrustCertificate(Certificate): def __init__(self, module, backend): super(EntrustCertificate, self).__init__(module, backend) self.trackingId = None - self.notAfter = crypto_utils.get_relative_time_option(module.params['entrust_not_after'], 'entrust_not_after', backend=self.backend) + self.notAfter = get_relative_time_option(module.params['entrust_not_after'], 'entrust_not_after', backend=self.backend) if self.csr_content is None or not os.path.exists(self.csr_path): raise CertificateError( 'The certificate signing request file {0} does not exist'.format(self.csr_path) ) - self.csr = crypto_utils.load_certificate_request( + self.csr = load_certificate_request( path=self.csr_path, content=self.csr_content, backend=self.backend, @@ -2339,8 +2367,8 @@ class EntrustCertificate(Certificate): if self.backup: self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, to_bytes(result.get('endEntityCert'))) - self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) + write_file(module, to_bytes(result.get('endEntityCert'))) + self.cert = load_certificate(self.path, backend=self.backend) self.changed = True def check(self, module, perms_required=True): @@ -2374,7 +2402,7 @@ class EntrustCertificate(Certificate): time_string = to_native(self.cert.get_notAfter()) expiry = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ") elif self.backend == 'cryptography': - serial_number = "{0:X}".format(self.cert.serial_number) + serial_number = "{0:X}".format(cryptography_serial_number_of_cert(self.cert)) expiry = self.cert.not_valid_after # get some information about the expiry of this certificate @@ -2409,7 +2437,7 @@ class EntrustCertificate(Certificate): if self.backup_file: result['backup_file'] = self.backup_file if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + content = load_file_if_exists(self.path, ignore_errors=True) result['certificate'] = content.decode('utf-8') if content else None result.update(self._get_cert_details()) @@ -2480,7 +2508,7 @@ class AcmeCertificate(Certificate): crt = module.run_command(command, check_rc=True)[1] if self.backup: self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, to_bytes(crt)) + write_file(module, to_bytes(crt)) self.changed = True except OSError as exc: raise CertificateError(exc) @@ -2501,7 +2529,7 @@ class AcmeCertificate(Certificate): if self.backup_file: result['backup_file'] = self.backup_file if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) + content = load_file_if_exists(self.path, ignore_errors=True) result['certificate'] = content.decode('utf-8') if content else None return result @@ -2724,7 +2752,7 @@ def main(): result = certificate.dump() module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: + except OpenSSLObjectError as exc: module.fail_json(msg=to_native(exc)) diff --git a/plugins/modules/x509_certificate_info.py b/plugins/modules/x509_certificate_info.py index f1aec54e..6a084fb2 100644 --- a/plugins/modules/x509_certificate_info.py +++ b/plugins/modules/x509_certificate_info.py @@ -306,9 +306,31 @@ from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.six import string_types from ansible.module_utils._text import to_native, to_text, to_bytes -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + get_relative_time_option, + load_certificate, + get_fingerprint_of_bytes, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_decode_name, + cryptography_get_extensions_from_cert, + cryptography_oid_to_name, + cryptography_serial_number_of_cert, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import ( + pyopenssl_get_extensions_from_cert, + pyopenssl_normalize_name, +) + MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' MINIMAL_PYOPENSSL_VERSION = '0.15' @@ -347,7 +369,7 @@ else: TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" -class CertificateInfo(crypto_utils.OpenSSLObject): +class CertificateInfo(OpenSSLObject): def __init__(self, module, backend): super(CertificateInfo, self).__init__( module.params['path'] or '', @@ -368,14 +390,14 @@ class CertificateInfo(crypto_utils.OpenSSLObject): self.module.fail_json( msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v)) ) - self.valid_at[k] = crypto_utils.get_relative_time_option(v, 'valid_at.{0}'.format(k)) + self.valid_at[k] = get_relative_time_option(v, 'valid_at.{0}'.format(k)) def generate(self): - # Empty method because crypto_utils.OpenSSLObject wants this + # Empty method because OpenSSLObject wants this pass def dump(self): - # Empty method because crypto_utils.OpenSSLObject wants this + # Empty method because OpenSSLObject wants this pass @abc.abstractmethod @@ -448,7 +470,7 @@ class CertificateInfo(crypto_utils.OpenSSLObject): def get_info(self): result = dict() - self.cert = crypto_utils.load_certificate(self.path, content=self.content, backend=self.backend) + self.cert = load_certificate(self.path, content=self.content, backend=self.backend) result['signature_algorithm'] = self._get_signature_algorithm() subject = self._get_subject_ordered() @@ -481,7 +503,7 @@ class CertificateInfo(crypto_utils.OpenSSLObject): result['public_key'] = self._get_public_key(binary=False) pk = self._get_public_key(binary=True) - result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict() + result['public_key_fingerprints'] = get_fingerprint_of_bytes(pk) if pk is not None else dict() if self.backend != 'pyopenssl': ski = self._get_subject_key_identifier() @@ -511,18 +533,18 @@ class CertificateInfoCryptography(CertificateInfo): super(CertificateInfoCryptography, self).__init__(module, 'cryptography') def _get_signature_algorithm(self): - return crypto_utils.cryptography_oid_to_name(self.cert.signature_algorithm_oid) + return cryptography_oid_to_name(self.cert.signature_algorithm_oid) def _get_subject_ordered(self): result = [] for attribute in self.cert.subject: - result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) + result.append([cryptography_oid_to_name(attribute.oid), attribute.value]) return result def _get_issuer_ordered(self): result = [] for attribute in self.cert.issuer: - result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) + result.append([cryptography_oid_to_name(attribute.oid), attribute.value]) return result def _get_version(self): @@ -574,7 +596,7 @@ class CertificateInfoCryptography(CertificateInfo): try: ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage) return sorted([ - crypto_utils.cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value + cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value ]), ext_keyusage_ext.critical except cryptography.x509.ExtensionNotFound: return None, False @@ -608,7 +630,7 @@ class CertificateInfoCryptography(CertificateInfo): def _get_subject_alt_name(self): try: san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) - result = [crypto_utils.cryptography_decode_name(san) for san in san_ext.value] + result = [cryptography_decode_name(san) for san in san_ext.value] return result, san_ext.critical except cryptography.x509.ExtensionNotFound: return None, False @@ -637,16 +659,16 @@ class CertificateInfoCryptography(CertificateInfo): ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) issuer = None if ext.value.authority_cert_issuer is not None: - issuer = [crypto_utils.cryptography_decode_name(san) for san in ext.value.authority_cert_issuer] + issuer = [cryptography_decode_name(san) for san in ext.value.authority_cert_issuer] return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number except cryptography.x509.ExtensionNotFound: return None, None, None def _get_serial_number(self): - return self.cert.serial_number + return cryptography_serial_number_of_cert(self.cert) def _get_all_extensions(self): - return crypto_utils.cryptography_get_extensions_from_cert(self.cert) + return cryptography_get_extensions_from_cert(self.cert) def _get_ocsp_uri(self): try: @@ -672,7 +694,7 @@ class CertificateInfoPyOpenSSL(CertificateInfo): def __get_name(self, name): result = [] for sub in name.get_components(): - result.append([crypto_utils.pyopenssl_normalize_name(sub[0]), to_text(sub[1])]) + result.append([pyopenssl_normalize_name(sub[0]), to_text(sub[1])]) return result def _get_subject_ordered(self): @@ -691,7 +713,7 @@ class CertificateInfoPyOpenSSL(CertificateInfo): extension = self.cert.get_extension(extension_idx) if extension.get_short_name() == short_name: result = [ - crypto_utils.pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',') + pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',') ] return sorted(result), bool(extension.get_critical()) return None, False @@ -777,7 +799,7 @@ class CertificateInfoPyOpenSSL(CertificateInfo): return self.cert.get_serial_number() def _get_all_extensions(self): - return crypto_utils.pyopenssl_get_extensions_from_cert(self.cert) + return pyopenssl_get_extensions_from_cert(self.cert) def _get_ocsp_uri(self): for i in range(self.cert.get_extension_count()): @@ -856,7 +878,7 @@ def main(): result = certificate.get_info() module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: + except OpenSSLObjectError as exc: module.fail_json(msg=to_native(exc)) diff --git a/plugins/modules/x509_crl.py b/plugins/modules/x509_crl.py index ba7646e3..d3e9303f 100644 --- a/plugins/modules/x509_crl.py +++ b/plugins/modules/x509_crl.py @@ -354,7 +354,38 @@ from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native, to_text -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.module_utils.io import ( + write_file, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, + load_privatekey, + load_certificate, + parse_name_field, + get_relative_time_option, + select_message_digest, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_get_name, + cryptography_name_to_oid, + cryptography_oid_to_name, + cryptography_serial_number_of_cert, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import ( + REVOCATION_REASON_MAP, + TIMESTAMP_FORMAT, + cryptography_decode_revoked_certificate, + cryptography_dump_revoked, + cryptography_get_signature_algorithm_oid_from_crl, +) MINIMAL_CRYPTOGRAPHY_VERSION = '1.2' @@ -378,14 +409,11 @@ else: CRYPTOGRAPHY_FOUND = True -TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" - - -class CRLError(crypto_utils.OpenSSLObjectError): +class CRLError(OpenSSLObjectError): pass -class CRL(crypto_utils.OpenSSLObject): +class CRL(OpenSSLObject): def __init__(self, module): super(CRL, self).__init__( @@ -406,13 +434,13 @@ class CRL(crypto_utils.OpenSSLObject): self.privatekey_content = self.privatekey_content.encode('utf-8') self.privatekey_passphrase = module.params['privatekey_passphrase'] - self.issuer = crypto_utils.parse_name_field(module.params['issuer']) + self.issuer = parse_name_field(module.params['issuer']) self.issuer = [(entry[0], entry[1]) for entry in self.issuer if entry[1]] - self.last_update = crypto_utils.get_relative_time_option(module.params['last_update'], 'last_update') - self.next_update = crypto_utils.get_relative_time_option(module.params['next_update'], 'next_update') + self.last_update = get_relative_time_option(module.params['last_update'], 'last_update') + self.next_update = get_relative_time_option(module.params['next_update'], 'next_update') - self.digest = crypto_utils.select_message_digest(module.params['digest']) + self.digest = select_message_digest(module.params['digest']) if self.digest is None: raise CRLError('The digest "{0}" is not supported'.format(module.params['digest'])) @@ -434,13 +462,9 @@ class CRL(crypto_utils.OpenSSLObject): try: if rc['content'] is not None: rc['content'] = rc['content'].encode('utf-8') - cert = crypto_utils.load_certificate(rc['path'], content=rc['content'], backend='cryptography') - try: - result['serial_number'] = cert.serial_number - except AttributeError: - # The property was called "serial" before cryptography 1.4 - result['serial_number'] = cert.serial - except crypto_utils.OpenSSLObjectError as e: + cert = load_certificate(rc['path'], content=rc['content'], backend='cryptography') + result['serial_number'] = cryptography_serial_number_of_cert(cert) + except OpenSSLObjectError as e: if rc['content'] is not None: module.fail_json( msg='Cannot parse certificate from {0}content: {1}'.format(path_prefix, to_native(e)) @@ -454,17 +478,17 @@ class CRL(crypto_utils.OpenSSLObject): result['serial_number'] = rc['serial_number'] # All other options if rc['issuer']: - result['issuer'] = [crypto_utils.cryptography_get_name(issuer) for issuer in rc['issuer']] + result['issuer'] = [cryptography_get_name(issuer) for issuer in rc['issuer']] result['issuer_critical'] = rc['issuer_critical'] - result['revocation_date'] = crypto_utils.get_relative_time_option( + result['revocation_date'] = get_relative_time_option( rc['revocation_date'], path_prefix + 'revocation_date' ) if rc['reason']: - result['reason'] = crypto_utils.REVOCATION_REASON_MAP[rc['reason']] + result['reason'] = REVOCATION_REASON_MAP[rc['reason']] result['reason_critical'] = rc['reason_critical'] if rc['invalidity_date']: - result['invalidity_date'] = crypto_utils.get_relative_time_option( + result['invalidity_date'] = get_relative_time_option( rc['invalidity_date'], path_prefix + 'invalidity_date' ) @@ -477,13 +501,13 @@ class CRL(crypto_utils.OpenSSLObject): self.backup_file = None try: - self.privatekey = crypto_utils.load_privatekey( + self.privatekey = load_privatekey( path=self.privatekey_path, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend='cryptography' ) - except crypto_utils.OpenSSLBadPassphraseError as exc: + except OpenSSLBadPassphraseError as exc: raise CRLError(exc) self.crl = None @@ -543,11 +567,11 @@ class CRL(crypto_utils.OpenSSLObject): if self.digest.name != self.crl.signature_hash_algorithm.name: return False - want_issuer = [(crypto_utils.cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer] + want_issuer = [(cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer] if want_issuer != [(sub.oid, sub.value) for sub in self.crl.issuer]: return False - old_entries = [self._compress_entry(crypto_utils.cryptography_decode_revoked_certificate(cert)) for cert in self.crl] + old_entries = [self._compress_entry(cryptography_decode_revoked_certificate(cert)) for cert in self.crl] new_entries = [self._compress_entry(cert) for cert in self.revoked_certificates] if self.update: # We don't simply use a set so that duplicate entries are treated correctly @@ -568,7 +592,7 @@ class CRL(crypto_utils.OpenSSLObject): try: crl = crl.issuer_name(Name([ - NameAttribute(crypto_utils.cryptography_name_to_oid(entry[0]), to_text(entry[1])) + NameAttribute(cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.issuer ])) except ValueError as e: @@ -580,7 +604,7 @@ class CRL(crypto_utils.OpenSSLObject): if self.update and self.crl: new_entries = set([self._compress_entry(entry) for entry in self.revoked_certificates]) for entry in self.crl: - decoded_entry = self._compress_entry(crypto_utils.cryptography_decode_revoked_certificate(entry)) + decoded_entry = self._compress_entry(cryptography_decode_revoked_certificate(entry)) if decoded_entry not in new_entries: crl = crl.add_revoked_certificate(entry) for entry in self.revoked_certificates: @@ -590,7 +614,7 @@ class CRL(crypto_utils.OpenSSLObject): if entry['issuer'] is not None: revoked_cert = revoked_cert.add_extension( x509.CertificateIssuer([ - crypto_utils.cryptography_get_name(name) for name in self.entry['issuer'] + cryptography_get_name(name) for name in entry['issuer'] ]), entry['issuer_critical'] ) @@ -616,29 +640,13 @@ class CRL(crypto_utils.OpenSSLObject): self.crl_content = result if self.backup: self.backup_file = self.module.backup_local(self.path) - crypto_utils.write_file(self.module, result) + write_file(self.module, result) self.changed = True file_args = self.module.load_file_common_arguments(self.module.params) if self.module.set_fs_attributes_if_different(file_args, False): self.changed = True - def _dump_revoked(self, entry): - return { - 'serial_number': entry['serial_number'], - 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT), - 'issuer': - [crypto_utils.cryptography_decode_name(issuer) for issuer in entry['issuer']] - if entry['issuer'] is not None else None, - 'issuer_critical': entry['issuer_critical'], - 'reason': crypto_utils.REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None, - 'reason_critical': entry['reason_critical'], - 'invalidity_date': - entry['invalidity_date'].strftime(TIMESTAMP_FORMAT) - if entry['invalidity_date'] is not None else None, - 'invalidity_date_critical': entry['invalidity_date_critical'], - } - def dump(self, check_mode=False): result = { 'changed': self.changed, @@ -657,7 +665,7 @@ class CRL(crypto_utils.OpenSSLObject): if check_mode: result['last_update'] = self.last_update.strftime(TIMESTAMP_FORMAT) result['next_update'] = self.next_update.strftime(TIMESTAMP_FORMAT) - # result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid) + # result['digest'] = cryptography_oid_to_name(self.crl.signature_algorithm_oid) result['digest'] = self.module.params['digest'] result['issuer_ordered'] = self.issuer result['issuer'] = {} @@ -665,32 +673,22 @@ class CRL(crypto_utils.OpenSSLObject): result['issuer'][k] = v result['revoked_certificates'] = [] for entry in self.revoked_certificates: - result['revoked_certificates'].append(self._dump_revoked(entry)) + result['revoked_certificates'].append(cryptography_dump_revoked(entry)) elif self.crl: result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT) result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT) - try: - result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid) - except AttributeError: - # Older cryptography versions don't have signature_algorithm_oid yet - dotted = crypto_utils._obj2txt( - self.crl._backend._lib, - self.crl._backend._ffi, - self.crl._x509_crl.sig_alg.algorithm - ) - oid = x509.oid.ObjectIdentifier(dotted) - result['digest'] = crypto_utils.cryptography_oid_to_name(oid) + result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl)) issuer = [] for attribute in self.crl.issuer: - issuer.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) + issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value]) result['issuer_ordered'] = issuer result['issuer'] = {} for k, v in issuer: result['issuer'][k] = v result['revoked_certificates'] = [] for cert in self.crl: - entry = crypto_utils.cryptography_decode_revoked_certificate(cert) - result['revoked_certificates'].append(self._dump_revoked(entry)) + entry = cryptography_decode_revoked_certificate(cert) + result['revoked_certificates'].append(cryptography_dump_revoked(entry)) if self.return_content: result['crl'] = self.crl_content @@ -776,7 +774,7 @@ def main(): result = crl.dump() module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: + except OpenSSLObjectError as exc: module.fail_json(msg=to_native(exc)) diff --git a/plugins/modules/x509_crl_info.py b/plugins/modules/x509_crl_info.py index d20cb5eb..f690b672 100644 --- a/plugins/modules/x509_crl_info.py +++ b/plugins/modules/x509_crl_info.py @@ -134,7 +134,26 @@ from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils._text import to_native -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_oid_to_name, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import ( + TIMESTAMP_FORMAT, + cryptography_decode_revoked_certificate, + cryptography_dump_revoked, + cryptography_get_signature_algorithm_oid_from_crl, +) + +# crypto_utils MINIMAL_CRYPTOGRAPHY_VERSION = '1.2' @@ -151,14 +170,11 @@ else: CRYPTOGRAPHY_FOUND = True -TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" - - -class CRLError(crypto_utils.OpenSSLObjectError): +class CRLError(OpenSSLObjectError): pass -class CRLInfo(crypto_utils.OpenSSLObject): +class CRLInfo(OpenSSLObject): """The main module implementation.""" def __init__(self, module): @@ -188,22 +204,6 @@ class CRLInfo(crypto_utils.OpenSSLObject): except Exception as e: self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e)) - def _dump_revoked(self, entry): - return { - 'serial_number': entry['serial_number'], - 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT), - 'issuer': - [crypto_utils.cryptography_decode_name(issuer) for issuer in entry['issuer']] - if entry['issuer'] is not None else None, - 'issuer_critical': entry['issuer_critical'], - 'reason': crypto_utils.REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None, - 'reason_critical': entry['reason_critical'], - 'invalidity_date': - entry['invalidity_date'].strftime(TIMESTAMP_FORMAT) - if entry['invalidity_date'] is not None else None, - 'invalidity_date_critical': entry['invalidity_date_critical'], - } - def get_info(self): result = { 'changed': False, @@ -217,37 +217,27 @@ class CRLInfo(crypto_utils.OpenSSLObject): result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT) result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT) - try: - result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid) - except AttributeError: - # Older cryptography versions don't have signature_algorithm_oid yet - dotted = crypto_utils._obj2txt( - self.crl._backend._lib, - self.crl._backend._ffi, - self.crl._x509_crl.sig_alg.algorithm - ) - oid = x509.oid.ObjectIdentifier(dotted) - result['digest'] = crypto_utils.cryptography_oid_to_name(oid) + result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl)) issuer = [] for attribute in self.crl.issuer: - issuer.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) + issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value]) result['issuer_ordered'] = issuer result['issuer'] = {} for k, v in issuer: result['issuer'][k] = v result['revoked_certificates'] = [] for cert in self.crl: - entry = crypto_utils.cryptography_decode_revoked_certificate(cert) - result['revoked_certificates'].append(self._dump_revoked(entry)) + entry = cryptography_decode_revoked_certificate(cert) + result['revoked_certificates'].append(cryptography_dump_revoked(entry)) return result def generate(self): - # Empty method because crypto_utils.OpenSSLObject wants this + # Empty method because OpenSSLObject wants this pass def dump(self): - # Empty method because crypto_utils.OpenSSLObject wants this + # Empty method because OpenSSLObject wants this pass @@ -274,7 +264,7 @@ def main(): crl = CRLInfo(module) result = crl.get_info() module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as e: + except OpenSSLObjectError as e: module.fail_json(msg=to_native(e)) diff --git a/tests/sanity/ignore-2.10.txt b/tests/sanity/ignore-2.10.txt index 0eb60c28..c9f2cb39 100644 --- a/tests/sanity/ignore-2.10.txt +++ b/tests/sanity/ignore-2.10.txt @@ -2,6 +2,7 @@ plugins/module_utils/compat/ipaddress.py future-import-boilerplate plugins/module_utils/compat/ipaddress.py metaclass-boilerplate plugins/module_utils/compat/ipaddress.py no-assert plugins/module_utils/compat/ipaddress.py no-unicode-literals +plugins/module_utils/crypto/__init__.py empty-init plugins/modules/acme_account_info.py validate-modules:return-syntax-error plugins/modules/acme_certificate.py validate-modules:doc-elements-mismatch tests/unit/mock/path.py future-import-boilerplate diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index 7982d71d..6fabef25 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -2,6 +2,7 @@ plugins/module_utils/compat/ipaddress.py future-import-boilerplate plugins/module_utils/compat/ipaddress.py metaclass-boilerplate plugins/module_utils/compat/ipaddress.py no-assert plugins/module_utils/compat/ipaddress.py no-unicode-literals +plugins/module_utils/crypto/__init__.py empty-init tests/unit/mock/path.py future-import-boilerplate tests/unit/mock/path.py metaclass-boilerplate tests/unit/mock/yaml_helper.py future-import-boilerplate