diff --git a/changelogs/fragments/830-x509-convert-verify.yml b/changelogs/fragments/830-x509-convert-verify.yml new file mode 100644 index 000000000..ef3a803c5 --- /dev/null +++ b/changelogs/fragments/830-x509-convert-verify.yml @@ -0,0 +1,3 @@ +minor_changes: + - "x509_certificate_convert - add new option ``verify_cert_parsable`` which allows to check whether the certificate can actually be parsed + (https://github.com/ansible-collections/community.crypto/issues/809, https://github.com/ansible-collections/community.crypto/pull/830)." diff --git a/plugins/modules/x509_certificate_convert.py b/plugins/modules/x509_certificate_convert.py index c9f1c6ab5..2886a3404 100644 --- a/plugins/modules/x509_certificate_convert.py +++ b/plugins/modules/x509_certificate_convert.py @@ -59,6 +59,7 @@ description: - If the input is a PEM file, ensure that it contains a single PEM object, that the header and footer match, and are of type C(CERTIFICATE) or C(X509 CERTIFICATE). + - See also the O(verify_cert_parsable) option, which checks whether the certificate is parsable. type: bool default: false dest_path: @@ -72,12 +73,21 @@ with a new one by accident. type: bool default: false + verify_cert_parsable: + description: + - If set to V(true), ensures that the certificate can be parsed. + - To ensure that a PEM file does not contain multiple certificates, use the O(strict) option. + type: bool + default: false + version_added: 2.23.0 seealso: - plugin: ansible.builtin.b64encode plugin_type: filter - module: community.crypto.x509_certificate - module: community.crypto.x509_certificate_pipe - module: community.crypto.x509_certificate_info +requirements: + - cryptography >= 1.6 if O(verify_cert_parsable=true) """ EXAMPLES = r""" @@ -98,8 +108,9 @@ import base64 import os +import traceback -from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import AnsibleModule, missing_required_lib from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text from ansible_collections.community.crypto.plugins.module_utils.io import ( @@ -124,6 +135,19 @@ OpenSSLObject, ) +MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' + +CRYPTOGRAPHY_IMP_ERR = None +try: + import cryptography # noqa: F401, pylint: disable=unused-import + from cryptography.x509 import load_der_x509_certificate + from cryptography.hazmat.backends import default_backend +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + def parse_certificate(input, strict=False): input_format = 'pem' if identify_pem_format(input) else 'der' @@ -175,6 +199,9 @@ def __init__(self, module): except Exception as exc: module.fail_json(msg='Error while parsing PEM: {exc}'.format(exc=exc)) + if module.params['verify_cert_parsable']: + self.verify_cert_parsable(module) + self.backup = module.params['backup'] self.backup_file = None @@ -190,6 +217,17 @@ def __init__(self, module): except Exception: pass + def verify_cert_parsable(self, module): + if not CRYPTOGRAPHY_FOUND: + module.fail_json( + msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), + exception=CRYPTOGRAPHY_IMP_ERR, + ) + try: + load_der_x509_certificate(self.input, default_backend()) + except Exception as exc: + module.fail_json(msg='Error while parsing certificate: {exc}'.format(exc=exc)) + def needs_conversion(self): if self.dest_content is None or self.dest_content_format is None: return True @@ -247,6 +285,7 @@ def main(): strict=dict(type='bool', default=False), dest_path=dict(type='path', required=True), backup=dict(type='bool', default=False), + verify_cert_parsable=dict(type='bool', default=False), ) module = AnsibleModule( argument_spec, diff --git a/tests/integration/targets/x509_certificate_convert/tasks/impl.yml b/tests/integration/targets/x509_certificate_convert/tasks/impl.yml index e0c438937..bd31aae49 100644 --- a/tests/integration/targets/x509_certificate_convert/tasks/impl.yml +++ b/tests/integration/targets/x509_certificate_convert/tasks/impl.yml @@ -210,3 +210,33 @@ - result_8 is not changed - result_9 is not changed - result_10 is not changed + +- name: Create empty file + ansible.builtin.copy: + dest: '{{ remote_tmp_dir }}/empty' + content: '' + +- name: Convert empty file to PEM (w/o verify) + community.crypto.x509_certificate_convert: + src_path: '{{ remote_tmp_dir }}/empty' + dest_path: '{{ remote_tmp_dir }}/empty.pem' + format: pem + verify_cert_parsable: false + register: result_1 + +- name: Convert empty file to PEM (w/ verify) + community.crypto.x509_certificate_convert: + src_path: '{{ remote_tmp_dir }}/empty' + dest_path: '{{ remote_tmp_dir }}/empty.pem' + format: pem + verify_cert_parsable: true + register: result_2 + ignore_errors: true + +- name: Check conditions + assert: + that: + - result_1 is changed + - result_2 is failed + - >- + result_2.msg.startswith('Error while parsing certificate: ')