From b2ea4a7ce59e6db8001890c60864b32e05522828 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Wed, 5 Jan 2022 18:19:42 +0100 Subject: [PATCH] Add basic crypto_info module (#363) * Add basic crypto_info module. * Improve check. * Actually test capabilities. * Also output EC curve list. * Fix detections. * Ed25519 and Ed448 are not supported on FreeBSD 12.1. * Refactor. * Also retrieve information on the OpenSSL binary. * Improve splitting. * Update plugins/modules/crypto_info.py Co-authored-by: Andrew Pantuso * Replace list by tuple. Co-authored-by: Andrew Pantuso --- plugins/modules/crypto_info.py | 335 ++++++++++++++++++ tests/integration/targets/crypto_info/aliases | 4 + .../targets/crypto_info/meta/main.yml | 2 + .../targets/crypto_info/tasks/main.yml | 75 ++++ 4 files changed, 416 insertions(+) create mode 100644 plugins/modules/crypto_info.py create mode 100644 tests/integration/targets/crypto_info/aliases create mode 100644 tests/integration/targets/crypto_info/meta/main.yml create mode 100644 tests/integration/targets/crypto_info/tasks/main.yml diff --git a/plugins/modules/crypto_info.py b/plugins/modules/crypto_info.py new file mode 100644 index 00000000..0dce2c6d --- /dev/null +++ b/plugins/modules/crypto_info.py @@ -0,0 +1,335 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2021 Felix Fontein +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = ''' +--- +module: crypto_info +author: "Felix Fontein (@felixfontein)" +short_description: Retrieve cryptographic capabilities +version_added: 2.1.0 +description: + - Retrieve information on cryptographic capabilities. + - The current version retrieves information on the L(Python cryptography library, https://cryptography.io/) available to + Ansible modules, and on the OpenSSL binary C(openssl) found in the path. +notes: + - Supports C(check_mode). +options: {} +''' + +EXAMPLES = ''' +- name: Retrieve information + community.crypto.crypto_info: + account_key_src: /etc/pki/cert/private/account.key + register: crypto_information + +- name: Show retrieved information + ansible.builtin.debug: + var: crypto_information +''' + +RETURN = ''' +python_cryptography_installed: + description: Whether the L(Python cryptography library, https://cryptography.io/) is installed. + returned: always + type: bool + sample: true + +python_cryptography_import_error: + description: Import error when trying to import the L(Python cryptography library, https://cryptography.io/). + returned: when I(python_cryptography_installed=false) + type: str + +python_cryptography_capabilities: + description: Information on the installed L(Python cryptography library, https://cryptography.io/). + returned: when I(python_cryptography_installed=true) + type: dict + contains: + version: + description: The library version. + type: str + curves: + description: + - List of all supported elliptic curves. + - Theoretically this should be non-empty for version 0.5 and higher, depending on the libssl version used. + type: list + elements: str + has_ec: + description: + - Whether elliptic curves are supported. + - Theoretically this should be the case for version 0.5 and higher, depending on the libssl version used. + type: bool + has_ec_sign: + description: + - Whether signing with elliptic curves is supported. + - Theoretically this should be the case for version 1.5 and higher, depending on the libssl version used. + type: bool + has_ed25519: + description: + - Whether Ed25519 keys are supported. + - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used. + type: bool + has_ed25519_sign: + description: + - Whether signing with Ed25519 keys is supported. + - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used. + type: bool + has_ed448: + description: + - Whether Ed448 keys are supported. + - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used. + type: bool + has_ed448_sign: + description: + - Whether signing with Ed448 keys is supported. + - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used. + type: bool + has_dsa: + description: + - Whether DSA keys are supported. + - Theoretically this should be the case for version 0.5 and higher. + type: bool + has_dsa_sign: + description: + - Whether signing with DSA keys is supported. + - Theoretically this should be the case for version 1.5 and higher. + type: bool + has_rsa: + description: + - Whether RSA keys are supported. + - Theoretically this should be the case for version 0.5 and higher. + type: bool + has_rsa_sign: + description: + - Whether signing with RSA keys is supported. + - Theoretically this should be the case for version 1.4 and higher. + type: bool + has_x25519: + description: + - Whether X25519 keys are supported. + - Theoretically this should be the case for version 2.0 and higher, depending on the libssl version used. + type: bool + has_x25519_serialization: + description: + - Whether serialization of X25519 keys is supported. + - Theoretically this should be the case for version 2.5 and higher, depending on the libssl version used. + type: bool + has_x448: + description: + - Whether X448 keys are supported. + - Theoretically this should be the case for version 2.5 and higher, depending on the libssl version used. + type: bool + +openssl_present: + description: Whether the OpenSSL binary C(openssl) is installed and can be found in the PATH. + returned: always + type: bool + sample: true + +openssl: + description: Information on the installed OpenSSL binary. + returned: when I(openssl_present=true) + type: dict + contains: + path: + description: Path of the OpenSSL binary. + type: str + sample: /usr/bin/openssl + version: + description: The OpenSSL version. + type: str + sample: 1.1.1m + version_output: + description: The complete output of C(openssl version). + type: str + sample: 'OpenSSL 1.1.1m 14 Dec 2021\n' +''' + +import traceback + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + CRYPTOGRAPHY_HAS_EC, + CRYPTOGRAPHY_HAS_EC_SIGN, + CRYPTOGRAPHY_HAS_ED25519, + CRYPTOGRAPHY_HAS_ED25519_SIGN, + CRYPTOGRAPHY_HAS_ED448, + CRYPTOGRAPHY_HAS_ED448_SIGN, + CRYPTOGRAPHY_HAS_DSA, + CRYPTOGRAPHY_HAS_DSA_SIGN, + CRYPTOGRAPHY_HAS_RSA, + CRYPTOGRAPHY_HAS_RSA_SIGN, + CRYPTOGRAPHY_HAS_X25519, + CRYPTOGRAPHY_HAS_X25519_FULL, + CRYPTOGRAPHY_HAS_X448, + HAS_CRYPTOGRAPHY, +) + +try: + import cryptography + from cryptography.exceptions import UnsupportedAlgorithm +except ImportError: + UnsupportedAlgorithm = Exception + CRYPTOGRAPHY_VERSION = None + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() +else: + CRYPTOGRAPHY_VERSION = cryptography.__version__ + CRYPTOGRAPHY_IMP_ERR = None + + +CURVES = ( + ('secp224r1', 'SECP224R1'), + ('secp256k1', 'SECP256K1'), + ('secp256r1', 'SECP256R1'), + ('secp384r1', 'SECP384R1'), + ('secp521r1', 'SECP521R1'), + ('secp192r1', 'SECP192R1'), + ('sect163k1', 'SECT163K1'), + ('sect163r2', 'SECT163R2'), + ('sect233k1', 'SECT233K1'), + ('sect233r1', 'SECT233R1'), + ('sect283k1', 'SECT283K1'), + ('sect283r1', 'SECT283R1'), + ('sect409k1', 'SECT409K1'), + ('sect409r1', 'SECT409R1'), + ('sect571k1', 'SECT571K1'), + ('sect571r1', 'SECT571R1'), + ('brainpoolP256r1', 'BrainpoolP256R1'), + ('brainpoolP384r1', 'BrainpoolP384R1'), + ('brainpoolP512r1', 'BrainpoolP512R1'), +) + + +def add_crypto_information(module): + result = {} + result['python_cryptography_installed'] = HAS_CRYPTOGRAPHY + if not HAS_CRYPTOGRAPHY: + result['python_cryptography_import_error'] = CRYPTOGRAPHY_IMP_ERR + return result + + has_ed25519 = CRYPTOGRAPHY_HAS_ED25519 + if has_ed25519: + try: + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + Ed25519PrivateKey.from_private_bytes(b'') + except ValueError: + pass + except UnsupportedAlgorithm: + has_ed25519 = False + + has_ed448 = CRYPTOGRAPHY_HAS_ED448 + if has_ed448: + try: + from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey + Ed448PrivateKey.from_private_bytes(b'') + except ValueError: + pass + except UnsupportedAlgorithm: + has_ed448 = False + + has_x25519 = CRYPTOGRAPHY_HAS_X25519 + if has_x25519: + try: + from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey + if CRYPTOGRAPHY_HAS_X25519_FULL: + X25519PrivateKey.from_private_bytes(b'') + else: + # Some versions do not support serialization and deserialization - use generate() instead + X25519PrivateKey.generate() + except ValueError: + pass + except UnsupportedAlgorithm: + has_x25519 = False + + has_x448 = CRYPTOGRAPHY_HAS_X448 + if has_x448: + try: + from cryptography.hazmat.primitives.asymmetric.x448 import X448PrivateKey + X448PrivateKey.from_private_bytes(b'') + except ValueError: + pass + except UnsupportedAlgorithm: + has_x448 = False + + curves = [] + if CRYPTOGRAPHY_HAS_EC: + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.asymmetric.ec + + backend = cryptography.hazmat.backends.default_backend() + for curve_name, constructor_name in CURVES: + ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(constructor_name) + if ecclass: + try: + cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(curve=ecclass(), backend=backend) + curves.append(curve_name) + except UnsupportedAlgorithm: + pass + + info = { + 'version': CRYPTOGRAPHY_VERSION, + 'curves': curves, + 'has_ec': CRYPTOGRAPHY_HAS_EC, + 'has_ec_sign': CRYPTOGRAPHY_HAS_EC_SIGN, + 'has_ed25519': has_ed25519, + 'has_ed25519_sign': has_ed25519 and CRYPTOGRAPHY_HAS_ED25519_SIGN, + 'has_ed448': has_ed448, + 'has_ed448_sign': has_ed448 and CRYPTOGRAPHY_HAS_ED448_SIGN, + 'has_dsa': CRYPTOGRAPHY_HAS_DSA, + 'has_dsa_sign': CRYPTOGRAPHY_HAS_DSA_SIGN, + 'has_rsa': CRYPTOGRAPHY_HAS_RSA, + 'has_rsa_sign': CRYPTOGRAPHY_HAS_RSA_SIGN, + 'has_x25519': has_x25519, + 'has_x25519_serialization': has_x25519 and CRYPTOGRAPHY_HAS_X25519_FULL, + 'has_x448': has_x448, + } + result['python_cryptography_capabilities'] = info + return result + + +def add_openssl_information(module): + openssl_binary = module.get_bin_path('openssl') + result = { + 'openssl_present': openssl_binary is not None, + } + if openssl_binary is None: + return result + + openssl_result = { + 'path': openssl_binary, + } + result['openssl'] = openssl_result + + rc, out, err = module.run_command([openssl_binary, 'version']) + if rc == 0: + openssl_result['version_output'] = out + parts = out.split(None, 2) + if len(parts) > 1: + openssl_result['version'] = parts[1] + + return result + + +INFO_FUNCTIONS = ( + add_crypto_information, + add_openssl_information, +) + + +def main(): + module = AnsibleModule(argument_spec={}, supports_check_mode=True) + result = {} + for fn in INFO_FUNCTIONS: + result.update(fn(module)) + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/crypto_info/aliases b/tests/integration/targets/crypto_info/aliases new file mode 100644 index 00000000..4ff07c7d --- /dev/null +++ b/tests/integration/targets/crypto_info/aliases @@ -0,0 +1,4 @@ +context/controller +shippable/cloud/group1 +shippable/posix/group1 +destructive diff --git a/tests/integration/targets/crypto_info/meta/main.yml b/tests/integration/targets/crypto_info/meta/main.yml new file mode 100644 index 00000000..800aff64 --- /dev/null +++ b/tests/integration/targets/crypto_info/meta/main.yml @@ -0,0 +1,2 @@ +dependencies: + - setup_openssl diff --git a/tests/integration/targets/crypto_info/tasks/main.yml b/tests/integration/targets/crypto_info/tasks/main.yml new file mode 100644 index 00000000..130fcade --- /dev/null +++ b/tests/integration/targets/crypto_info/tasks/main.yml @@ -0,0 +1,75 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Retrieve information + crypto_info: + register: result + +- name: Display information + debug: + var: result + +- name: Register cryptography version + command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'" + register: local_cryptography_version + +- name: Determine complex version-based capabilities + set_fact: + supports_ed25519: >- + {{ + local_cryptography_version.stdout is version("2.6", ">=") + and not ( + ansible_os_family == "FreeBSD" and + ansible_facts.distribution_version is version("12.1", ">=") and + ansible_facts.distribution_version is version("12.2", "<") + ) + }} + supports_ed448: >- + {{ + local_cryptography_version.stdout is version("2.6", ">=") + and not ( + ansible_os_family == "FreeBSD" and + ansible_facts.distribution_version is version("12.1", ">=") and + ansible_facts.distribution_version is version("12.2", "<") + ) + }} + +- name: Verify cryptography information + assert: + that: + - result.python_cryptography_installed + - "'python_cryptography_import_error' not in result" + - result.python_cryptography_capabilities.version == local_cryptography_version.stdout + - "'secp256r1' in result.python_cryptography_capabilities.curves" + - result.python_cryptography_capabilities.has_ec == (local_cryptography_version.stdout is version('0.5', '>=')) + - result.python_cryptography_capabilities.has_ec_sign == (local_cryptography_version.stdout is version('1.5', '>=')) + - result.python_cryptography_capabilities.has_ed25519 == supports_ed25519 + - result.python_cryptography_capabilities.has_ed25519_sign == supports_ed25519 + - result.python_cryptography_capabilities.has_ed448 == supports_ed448 + - result.python_cryptography_capabilities.has_ed448_sign == supports_ed448 + - result.python_cryptography_capabilities.has_dsa == (local_cryptography_version.stdout is version('0.5', '>=')) + - result.python_cryptography_capabilities.has_dsa_sign == (local_cryptography_version.stdout is version('1.5', '>=')) + - result.python_cryptography_capabilities.has_rsa == (local_cryptography_version.stdout is version('0.5', '>=')) + - result.python_cryptography_capabilities.has_rsa_sign == (local_cryptography_version.stdout is version('1.4', '>=')) + - result.python_cryptography_capabilities.has_x25519 == (local_cryptography_version.stdout is version('2.0', '>=')) + - result.python_cryptography_capabilities.has_x25519_serialization == (local_cryptography_version.stdout is version('2.5', '>=')) + - result.python_cryptography_capabilities.has_x448 == (local_cryptography_version.stdout is version('2.5', '>=')) + +- name: Find OpenSSL binary + command: which openssl + register: local_openssl_path + +- name: Find OpenSSL version + command: openssl version + register: local_openssl_version_full + +- name: Verify OpenSSL information + assert: + that: + - result.openssl_present + - result.openssl.path == local_openssl_path.stdout + - (result.openssl.version_output | trim) == local_openssl_version_full.stdout + - result.openssl.version == local_openssl_version_full.stdout.split(' ')[1]