From bd2bd79497505ab3e2d72b93f66fb82ff9d9946d Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 10 Jan 2022 21:01:52 +0100 Subject: [PATCH] Add openssl_privatekey_convert module (#362) * Add openssl_privatekey_convert module. * Extend tests and fix bugs. * Fix wrong required. * Fix condition. * Fix bad tests. * Fix documentation for format. * Fix copyright lines. --- .../module_privatekey_convert.py | 47 +++ .../crypto/cryptography_support.py | 78 +++- .../module_backends/privatekey_convert.py | 235 +++++++++++ plugins/module_utils/io.py | 13 + plugins/modules/openssl_privatekey_convert.py | 161 ++++++++ .../openssl_privatekey_convert/aliases | 4 + .../openssl_privatekey_convert/meta/main.yml | 3 + .../openssl_privatekey_convert/tasks/impl.yml | 386 ++++++++++++++++++ .../openssl_privatekey_convert/tasks/main.yml | 61 +++ 9 files changed, 972 insertions(+), 16 deletions(-) create mode 100644 plugins/doc_fragments/module_privatekey_convert.py create mode 100644 plugins/module_utils/crypto/module_backends/privatekey_convert.py create mode 100644 plugins/modules/openssl_privatekey_convert.py create mode 100644 tests/integration/targets/openssl_privatekey_convert/aliases create mode 100644 tests/integration/targets/openssl_privatekey_convert/meta/main.yml create mode 100644 tests/integration/targets/openssl_privatekey_convert/tasks/impl.yml create mode 100644 tests/integration/targets/openssl_privatekey_convert/tasks/main.yml diff --git a/plugins/doc_fragments/module_privatekey_convert.py b/plugins/doc_fragments/module_privatekey_convert.py new file mode 100644 index 00000000..90bd56ce --- /dev/null +++ b/plugins/doc_fragments/module_privatekey_convert.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, 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 + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +requirements: + - cryptography >= 1.2.3 (older versions might work as well) +options: + src_path: + description: + - Name of the file containing the OpenSSL private key to convert. + - Exactly one of I(src_path) or I(src_content) must be specified. + type: path + src_content: + description: + - The content of the file containing the OpenSSL private key to convert. + - Exactly one of I(src_path) or I(src_content) must be specified. + type: str + src_passphrase: + description: + - The passphrase for the private key to load. + type: str + dest_passphrase: + description: + - The passphrase for the private key to store. + type: str + format: + description: + - Determines which format the destination private key should be written in. + - Please note that not every key can be exported in any format, and that not every + format supports encryption. + type: str + choices: [ pkcs1, pkcs8, raw ] + required: true +seealso: + - module: community.crypto.openssl_privatekey + - module: community.crypto.openssl_privatekey_pipe + - module: community.crypto.openssl_publickey +''' diff --git a/plugins/module_utils/crypto/cryptography_support.py b/plugins/module_utils/crypto/cryptography_support.py index 6ddb977c..98d0fb02 100644 --- a/plugins/module_utils/crypto/cryptography_support.py +++ b/plugins/module_utils/crypto/cryptography_support.py @@ -60,6 +60,9 @@ except ImportError: from .basic import ( CRYPTOGRAPHY_HAS_ED25519, CRYPTOGRAPHY_HAS_ED448, + CRYPTOGRAPHY_HAS_X25519, + CRYPTOGRAPHY_HAS_X25519_FULL, + CRYPTOGRAPHY_HAS_X448, OpenSSLObjectError, ) @@ -506,32 +509,75 @@ def cryptography_key_needs_digest_for_signing(key): return True +def _compare_public_keys(key1, key2, clazz): + a = isinstance(key1, clazz) + b = isinstance(key2, clazz) + if not (a or b): + return None + 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 + + 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 + res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey) + if res is not None: + return res 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 + res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey) + if res is not None: + return res return key1.public_numbers() == key2.public_numbers() +def _compare_private_keys(key1, key2, clazz, has_no_private_bytes=False): + a = isinstance(key1, clazz) + b = isinstance(key2, clazz) + if not (a or b): + return None + if not a or not b: + return False + if has_no_private_bytes: + # We do not have the private_bytes() function - compare associated public keys + return cryptography_compare_public_keys(a.public_key(), b.public_key()) + encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption() + a = key1.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm) + b = key2.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm) + return a == b + + +def cryptography_compare_private_keys(key1, key2): + '''Tests whether two private keys are the same. + + Needs special logic for Ed25519, X25519, and Ed448 keys, since they do not have private_numbers(). + ''' + if CRYPTOGRAPHY_HAS_ED25519: + res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey) + if res is not None: + return res + if CRYPTOGRAPHY_HAS_X25519: + res = _compare_private_keys( + key1, key2, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey, has_no_private_bytes=not CRYPTOGRAPHY_HAS_X25519_FULL) + if res is not None: + return res + if CRYPTOGRAPHY_HAS_ED448: + res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey) + if res is not None: + return res + if CRYPTOGRAPHY_HAS_X448: + res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey) + if res is not None: + return res + return key1.private_numbers() == key2.private_numbers() + + def cryptography_serial_number_of_cert(cert): '''Returns cert.serial_number. diff --git a/plugins/module_utils/crypto/module_backends/privatekey_convert.py b/plugins/module_utils/crypto/module_backends/privatekey_convert.py new file mode 100644 index 00000000..fdb1b1d1 --- /dev/null +++ b/plugins/module_utils/crypto/module_backends/privatekey_convert.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- +# +# Copyright: (c) 2022, 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 + + +import abc +import traceback + +from ansible.module_utils import six +from ansible.module_utils.basic import missing_required_lib +from ansible.module_utils.common.text.converters import to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion + +from ansible_collections.community.crypto.plugins.module_utils.io import ( + load_file, +) + +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.cryptography_support import ( + cryptography_compare_private_keys, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import ( + identify_private_key_format, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec + + +MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography + import cryptography.exceptions + import cryptography.hazmat.backends + import cryptography.hazmat.primitives.serialization + import cryptography.hazmat.primitives.asymmetric.rsa + import cryptography.hazmat.primitives.asymmetric.dsa + import cryptography.hazmat.primitives.asymmetric.ec + import cryptography.hazmat.primitives.asymmetric.utils + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + + +class PrivateKeyError(OpenSSLObjectError): + pass + + +# From the object called `module`, only the following properties are used: +# +# - module.params[] +# - module.warn(msg: str) +# - module.fail_json(msg: str, **kwargs) + + +@six.add_metaclass(abc.ABCMeta) +class PrivateKeyConvertBackend: + def __init__(self, module, backend): + self.module = module + self.src_path = module.params['src_path'] + self.src_content = module.params['src_content'] + self.src_passphrase = module.params['src_passphrase'] + self.format = module.params['format'] + self.dest_passphrase = module.params['dest_passphrase'] + self.backend = backend + + self.src_private_key = None + if self.src_path is not None: + self.src_private_key_bytes = load_file(self.src_path, module) + else: + self.src_private_key_bytes = self.src_content.encode('utf-8') + + self.dest_private_key = None + self.dest_private_key_bytes = None + + @abc.abstractmethod + def get_private_key_data(self): + """Return bytes for self.src_private_key in output format.""" + pass + + def set_existing_destination(self, privatekey_bytes): + """Set existing private key bytes. None indicates that the key does not exist.""" + self.dest_private_key_bytes = privatekey_bytes + + def has_existing_destination(self): + """Query whether an existing private key is/has been there.""" + return self.dest_private_key_bytes is not None + + @abc.abstractmethod + def _load_private_key(self, data, passphrase, current_hint=None): + """Check whether data cna be loaded as a private key with the provided passphrase. Return tuple (type, private_key).""" + pass + + def needs_conversion(self): + """Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False.""" + dummy, self.src_private_key = self._load_private_key(self.src_private_key_bytes, self.src_passphrase) + + if not self.has_existing_destination(): + return True + + try: + format, self.dest_private_key = self._load_private_key(self.dest_private_key_bytes, self.dest_passphrase, current_hint=self.src_private_key) + except Exception: + return True + + return format != self.format or not cryptography_compare_private_keys(self.dest_private_key, self.src_private_key) + + def dump(self): + """Serialize the object into a dictionary.""" + return {} + + +# Implementation with using cryptography +class PrivateKeyConvertCryptographyBackend(PrivateKeyConvertBackend): + def __init__(self, module): + super(PrivateKeyConvertCryptographyBackend, self).__init__(module=module, backend='cryptography') + + self.cryptography_backend = cryptography.hazmat.backends.default_backend() + + def get_private_key_data(self): + """Return bytes for self.src_private_key in output format""" + # Select export format and encoding + try: + export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM + if self.format == 'pkcs1': + # "TraditionalOpenSSL" format is PKCS1 + export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL + elif self.format == 'pkcs8': + export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8 + elif self.format == 'raw': + export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.Raw + export_encoding = cryptography.hazmat.primitives.serialization.Encoding.Raw + except AttributeError: + self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format)) + + # Select key encryption + encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption() + if self.dest_passphrase: + encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.dest_passphrase)) + + # Serialize key + try: + return self.src_private_key.private_bytes( + encoding=export_encoding, + format=export_format, + encryption_algorithm=encryption_algorithm + ) + except ValueError as dummy: + self.module.fail_json( + msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format) + ) + except Exception as dummy: + self.module.fail_json( + msg='Error while serializing the private key in the required format "{0}"'.format(self.format), + exception=traceback.format_exc() + ) + + def _load_private_key(self, data, passphrase, current_hint=None): + try: + # Interpret bytes depending on format. + format = identify_private_key_format(data) + if format == 'raw': + if passphrase is not None: + raise PrivateKeyError('Cannot load raw key with passphrase') + if len(data) == 56 and CRYPTOGRAPHY_HAS_X448: + return format, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data) + if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448: + return format, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data) + if len(data) == 32: + if CRYPTOGRAPHY_HAS_X25519 and not CRYPTOGRAPHY_HAS_ED25519: + return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) + if CRYPTOGRAPHY_HAS_ED25519 and not CRYPTOGRAPHY_HAS_X25519: + return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) + if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519: + if isinstance(current_hint, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey): + try: + return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) + except Exception: + return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) + else: + try: + return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) + except Exception: + return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) + raise PrivateKeyError('Cannot load raw key') + else: + return format, cryptography.hazmat.primitives.serialization.load_pem_private_key( + data, + None if passphrase is None else to_bytes(passphrase), + backend=self.cryptography_backend + ) + except Exception as e: + raise PrivateKeyError(e) + + +def select_backend(module): + if not CRYPTOGRAPHY_FOUND: + module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR) + return PrivateKeyConvertCryptographyBackend(module) + + +def get_privatekey_argument_spec(): + return ArgumentSpec( + argument_spec=dict( + src_path=dict(type='path'), + src_content=dict(type='str'), + src_passphrase=dict(type='str', no_log=True), + dest_passphrase=dict(type='str', no_log=True), + format=dict(type='str', required=True, choices=['pkcs1', 'pkcs8', 'raw']), + ), + mutually_exclusive=[ + ['src_path', 'src_content'], + ], + required_one_of=[ + ['src_path', 'src_content'], + ], + ) diff --git a/plugins/module_utils/io.py b/plugins/module_utils/io.py index c1c2643a..0bab7ce5 100644 --- a/plugins/module_utils/io.py +++ b/plugins/module_utils/io.py @@ -24,6 +24,19 @@ import os import tempfile +def load_file(path, module=None): + ''' + Load the file as a bytes string. + ''' + try: + with open(path, 'rb') as f: + return f.read() + except Exception as exc: + if module is None: + raise + module.fail_json('Error while loading {0} - {1}'.format(path, str(exc))) + + 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. diff --git a/plugins/modules/openssl_privatekey_convert.py b/plugins/modules/openssl_privatekey_convert.py new file mode 100644 index 00000000..6b82459e --- /dev/null +++ b/plugins/modules/openssl_privatekey_convert.py @@ -0,0 +1,161 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2022, 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 = r''' +--- +module: openssl_privatekey_convert +short_description: Convert OpenSSL private keys +version_added: 2.1.0 +description: + - This module allows one to convert OpenSSL private keys. + - The default mode for the private key file will be C(0600) if I(mode) is not explicitly set. +author: + - Felix Fontein (@felixfontein) +options: + dest_path: + description: + - Name of the file in which the generated TLS/SSL private key will be written. It will have C(0600) mode + if I(mode) is not explicitly set. + type: path + required: true + backup: + description: + - Create a backup file including a timestamp so you can get + the original private key back if you overwrote it with a new one by accident. + type: bool + default: false +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.module_privatekey_convert +seealso: [] +''' + +EXAMPLES = r''' +- name: Convert private key to PKCS8 format with passphrase + community.crypto.openssl_privatekey_convert: + src_path: /etc/ssl/private/ansible.com.pem + dest_path: /etc/ssl/private/ansible.com.key + dest_passphrase: '{{ private_key_passphrase }}' + format: pkcs8 +''' + +RETURN = r''' +backup_file: + description: Name of backup file created. + returned: changed and if I(backup) is C(yes) + type: str + sample: /path/to/privatekey.pem.2019-03-09@11:22~ +''' + +import os + +from ansible.module_utils.common.text.converters import to_native + +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, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + OpenSSLObject, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_convert import ( + select_backend, + get_privatekey_argument_spec, +) + + +class PrivateKeyConvertModule(OpenSSLObject): + def __init__(self, module, module_backend): + super(PrivateKeyConvertModule, self).__init__( + module.params['dest_path'], + 'present', + False, + module.check_mode, + ) + self.module_backend = module_backend + + self.backup = module.params['backup'] + self.backup_file = None + + module.params['path'] = module.params['dest_path'] + if module.params['mode'] is None: + module.params['mode'] = '0600' + + module_backend.set_existing_destination(load_file_if_exists(self.path, module)) + + def generate(self, module): + """Do conversion.""" + + if self.module_backend.needs_conversion(): + # Convert + privatekey_data = self.module_backend.get_private_key_data() + if not self.check_mode: + if self.backup: + self.backup_file = module.backup_local(self.path) + write_file(module, privatekey_data, 0o600) + self.changed = True + + file_args = module.load_file_common_arguments(module.params) + if module.check_file_absent_if_check_mode(file_args['path']): + self.changed = True + else: + self.changed = module.set_fs_attributes_if_different(file_args, self.changed) + + def dump(self): + """Serialize the object into a dictionary.""" + + result = self.module_backend.dump() + result['changed'] = self.changed + if self.backup_file: + result['backup_file'] = self.backup_file + + return result + + +def main(): + + argument_spec = get_privatekey_argument_spec() + argument_spec.argument_spec.update(dict( + dest_path=dict(type='path', required=True), + backup=dict(type='bool', default=False), + )) + module = argument_spec.create_ansible_module( + supports_check_mode=True, + add_file_common_args=True, + ) + + base_dir = os.path.dirname(module.params['dest_path']) or '.' + if not os.path.isdir(base_dir): + module.fail_json( + name=base_dir, + msg='The directory %s does not exist or the file is not a directory' % base_dir + ) + + module_backend = select_backend(module=module) + + try: + private_key = PrivateKeyConvertModule(module, module_backend) + + private_key.generate(module) + + result = private_key.dump() + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == '__main__': + main() diff --git a/tests/integration/targets/openssl_privatekey_convert/aliases b/tests/integration/targets/openssl_privatekey_convert/aliases new file mode 100644 index 00000000..4ff07c7d --- /dev/null +++ b/tests/integration/targets/openssl_privatekey_convert/aliases @@ -0,0 +1,4 @@ +context/controller +shippable/cloud/group1 +shippable/posix/group1 +destructive diff --git a/tests/integration/targets/openssl_privatekey_convert/meta/main.yml b/tests/integration/targets/openssl_privatekey_convert/meta/main.yml new file mode 100644 index 00000000..7f98a190 --- /dev/null +++ b/tests/integration/targets/openssl_privatekey_convert/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - setup_openssl + - setup_remote_tmp_dir diff --git a/tests/integration/targets/openssl_privatekey_convert/tasks/impl.yml b/tests/integration/targets/openssl_privatekey_convert/tasks/impl.yml new file mode 100644 index 00000000..399fe247 --- /dev/null +++ b/tests/integration/targets/openssl_privatekey_convert/tasks/impl.yml @@ -0,0 +1,386 @@ +--- +- name: Convert (check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs8 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_check + check_mode: true + +- name: Convert + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs8 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert + +- assert: + that: + - convert_check is changed + - convert is changed + +- name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_1.pem' + register: convert_file_info_data + +- name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs8 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + +- name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs8 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed + +- name: Convert (change format, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem_check + check_mode: true + +- name: Convert (change format) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_not_idem_check is changed + - convert_not_idem is changed + - convert_file_info is changed + +- name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_1.pem' + register: convert_file_info_data + +- name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + +- name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter2 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed + +- name: Convert (change password, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter3 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem_check + check_mode: true + +- name: Convert (change password) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter3 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_not_idem_check is changed + - convert_not_idem is changed + - convert_file_info is changed + +- name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_1.pem' + register: convert_file_info_data + +- name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter3 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + +- name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + dest_passphrase: hunter3 + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed + +- name: Convert (remove password, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem_check + check_mode: true + +- name: Convert (remove password) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_not_idem_check is changed + - convert_not_idem is changed + - convert_file_info is changed + +- name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_1.pem' + register: convert_file_info_data + +- name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + +- name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_1.pem' + format: pkcs1 + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + +- name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + +- assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed + +- when: supports_ed25519 | bool + block: + - name: Convert (change format to raw, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_ed25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_2.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem_check + check_mode: true + + - name: Convert (change format to raw) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_ed25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_2.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem + + - assert: + that: + - convert_not_idem_check is changed + - convert_not_idem is changed + + - name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_2.pem' + register: convert_file_info_data + + - name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_ed25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_2.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + + - name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_ed25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_2.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + + - name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + + - assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed + +- when: supports_x25519 | bool + block: + - name: Convert (change format to raw, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_x25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_3.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem_check + check_mode: true + + - name: Convert (change format to raw) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_x25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_3.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_not_idem + + - assert: + that: + - convert_not_idem_check is changed + - convert_not_idem is changed + + - name: "({{ select_crypto_backend }}) Collect file information" + community.internal_test_tools.files_collect: + files: + - path: '{{ remote_tmp_dir }}/output_3.pem' + register: convert_file_info_data + + - name: Convert (idempotent, check mode) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_x25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_3.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem_check + check_mode: true + + - name: Convert (idempotent) + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_x25519.pem' + dest_path: '{{ remote_tmp_dir }}/output_3.pem' + format: raw + # select_crypto_backend: '{{ select_crypto_backend }}' + register: convert_idem + + - name: "({{ select_crypto_backend }}) Check whether file changed" + community.internal_test_tools.files_diff: + state: '{{ convert_file_info_data }}' + register: convert_file_info + + - assert: + that: + - convert_idem_check is not changed + - convert_idem is not changed + - convert_file_info is not changed diff --git a/tests/integration/targets/openssl_privatekey_convert/tasks/main.yml b/tests/integration/targets/openssl_privatekey_convert/tasks/main.yml new file mode 100644 index 00000000..37896a9c --- /dev/null +++ b/tests/integration/targets/openssl_privatekey_convert/tasks/main.yml @@ -0,0 +1,61 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Determine capabilities + set_fact: + supports_x25519: '{{ cryptography_version.stdout is version("2.5", ">=") }}' + supports_ed25519: >- + {{ + 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: Create keys + openssl_privatekey: + size: '{{ item.size | default(omit) }}' + path: '{{ remote_tmp_dir }}/privatekey_{{ item.name }}.pem' + type: '{{ item.type | default(omit) }}' + curve: '{{ item.curve | default(omit) }}' + passphrase: '{{ item.passphrase | default(omit) }}' + cipher: '{{ "auto" if item.passphrase is defined else omit }}' + format: '{{ item.format }}' + when: item.condition | default(true) + loop: + - name: rsa_pass1 + format: pkcs1 + type: RSA + size: '{{ default_rsa_key_size }}' + passphrase: secret + - name: ed25519 + format: pkcs8 + type: Ed25519 + size: '{{ default_rsa_key_size }}' + condition: '{{ supports_ed25519 }}' + - name: x25519 + format: pkcs8 + type: X25519 + size: '{{ default_rsa_key_size }}' + condition: '{{ supports_x25519 }}' + +- name: Run module with backend autodetection + openssl_privatekey_convert: + src_path: '{{ remote_tmp_dir }}/privatekey_rsa_pass1.pem' + src_passphrase: secret + dest_path: '{{ remote_tmp_dir }}/output_backend_selection.pem' + dest_passphrase: hunter2 + format: pkcs8 + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('1.2.3', '>=')