diff --git a/plugins/filter/openssl_privatekey_info.py b/plugins/filter/openssl_privatekey_info.py new file mode 100644 index 000000000..ed031dceb --- /dev/null +++ b/plugins/filter/openssl_privatekey_info.py @@ -0,0 +1,193 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2022, Felix Fontein +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +DOCUMENTATION = ''' +name: openssl_privatekey_info +short_description: Retrieve information from OpenSSL private keys +version_added: 2.10.0 +author: + - Felix Fontein (@felixfontein) +description: + - Provided an OpenSSL private keys, retrieve information. + - This is a filter version of the M(community.crypto.openssl_privatekey_info) module. +options: + _input: + description: + - The content of the OpenSSL private key. + type: string + required: true + passphrase: + description: + - The passphrase for the private key. + type: str + return_private_key_data: + description: + - Whether to return private key data. + - Only set this to C(true) when you want private information about this key to + leave the remote machine. + - "B(WARNING:) you have to make sure that private key data is not accidentally logged!" + type: bool + default: false +extends_documentation_fragment: + - community.crypto.name_encoding +seealso: + - module: community.crypto.openssl_privatekey_info +''' + +EXAMPLES = ''' +- name: Show the Subject Alt Names of the CSR + ansible.builtin.debug: + msg: >- + {{ + ( + lookup('ansible.builtin.file', '/path/to/cert.csr') + | community.crypto.openssl_privatekey_info + ).subject_alt_name | join(', ') + }} +''' + +RETURN = ''' +_value: + description: + - Information on the certificate. + type: dict + contains: + public_key: + description: Private key's public key in PEM format. + returned: success + type: str + sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..." + public_key_fingerprints: + description: + - Fingerprints of private key's public key. + - For every hash algorithm available, the fingerprint is computed. + returned: success + type: dict + sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63', + 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..." + type: + description: + - The key's type. + - One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448). + - Will start with C(unknown) if the key type cannot be determined. + returned: success + type: str + sample: RSA + public_data: + description: + - Public key data. Depends on key type. + returned: success + type: dict + contains: + size: + description: + - Bit size of modulus (RSA) or prime number (DSA). + type: int + returned: When C(type=RSA) or C(type=DSA) + modulus: + description: + - The RSA key's modulus. + type: int + returned: When C(type=RSA) + exponent: + description: + - The RSA key's public exponent. + type: int + returned: When C(type=RSA) + p: + description: + - The C(p) value for DSA. + - This is the prime modulus upon which arithmetic takes place. + type: int + returned: When C(type=DSA) + q: + description: + - The C(q) value for DSA. + - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the + multiplicative group of the prime field used. + type: int + returned: When C(type=DSA) + g: + description: + - The C(g) value for DSA. + - This is the element spanning the subgroup of the multiplicative group of the prime field used. + type: int + returned: When C(type=DSA) + curve: + description: + - The curve's name for ECC. + type: str + returned: When C(type=ECC) + exponent_size: + description: + - The maximum number of bits of a private key. This is basically the bit size of the subgroup used. + type: int + returned: When C(type=ECC) + x: + description: + - The C(x) coordinate for the public point on the elliptic curve. + type: int + returned: When C(type=ECC) + y: + description: + - For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve. + - For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key. + type: int + returned: When C(type=DSA) or C(type=ECC) + private_data: + description: + - Private key data. Depends on key type. + returned: success and when I(return_private_key_data) is set to C(true) + type: dict +''' + +from ansible.errors import AnsibleFilterError +from ansible.module_utils.six import string_types +from ansible.module_utils.common.text.converters import to_bytes, to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import ( + PrivateKeyParseError, + get_privatekey_info, +) + +from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock + + +def openssl_privatekey_info_filter(data, passphrase=None, return_private_key_data=False): + '''Extract information from X.509 PEM certificate.''' + if not isinstance(data, string_types): + raise AnsibleFilterError('The community.crypto.openssl_privatekey_info input must be a text type, not %s' % type(data)) + if passphrase is not None and not isinstance(passphrase, string_types): + raise AnsibleFilterError('The passphrase option must be a text type, not %s' % type(passphrase)) + if not isinstance(return_private_key_data, bool): + raise AnsibleFilterError('The return_private_key_data option must be a boolean, not %s' % type(return_private_key_data)) + + module = FilterModuleMock({}) + try: + result = get_privatekey_info(module, 'cryptography', content=to_bytes(data), passphrase=passphrase, return_private_key_data=return_private_key_data) + result.pop('can_parse_key', None) + result.pop('key_is_consistent', None) + return result + except PrivateKeyParseError as exc: + raise AnsibleFilterError(exc.error_message) + except OpenSSLObjectError as exc: + raise AnsibleFilterError(to_native(exc)) + + +class FilterModule(object): + '''Ansible jinja2 filters''' + + def filters(self): + return { + 'openssl_privatekey_info': openssl_privatekey_info_filter, + } diff --git a/plugins/module_utils/crypto/module_backends/privatekey_info.py b/plugins/module_utils/crypto/module_backends/privatekey_info.py index 5dd755ee3..d87b9c2be 100644 --- a/plugins/module_utils/crypto/module_backends/privatekey_info.py +++ b/plugins/module_utils/crypto/module_backends/privatekey_info.py @@ -214,7 +214,7 @@ def get_info(self, prefer_one_fingerprint=False): except OpenSSLObjectError as exc: raise PrivateKeyParseError(to_native(exc), result) - result['public_key'] = self._get_public_key(binary=False) + result['public_key'] = to_native(self._get_public_key(binary=False)) pk = self._get_public_key(binary=True) result['public_key_fingerprints'] = get_fingerprint_of_bytes( pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict() diff --git a/plugins/modules/openssl_privatekey_info.py b/plugins/modules/openssl_privatekey_info.py index 30870b28b..7eaec2348 100644 --- a/plugins/modules/openssl_privatekey_info.py +++ b/plugins/modules/openssl_privatekey_info.py @@ -76,6 +76,10 @@ seealso: - module: community.crypto.openssl_privatekey - module: community.crypto.openssl_privatekey_pipe + - ref: community.crypto.openssl_privatekey_info filter + # - plugin: community.crypto.openssl_privatekey_info + # plugin_type: filter + description: A filter variant of this module. ''' EXAMPLES = r''' diff --git a/tests/integration/targets/filter_openssl_privatekey_info/aliases b/tests/integration/targets/filter_openssl_privatekey_info/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/tests/integration/targets/filter_openssl_privatekey_info/aliases @@ -0,0 +1,7 @@ +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +azp/generic/2 +azp/posix/2 +destructive diff --git a/tests/integration/targets/filter_openssl_privatekey_info/meta/main.yml b/tests/integration/targets/filter_openssl_privatekey_info/meta/main.yml new file mode 100644 index 000000000..7c2b42405 --- /dev/null +++ b/tests/integration/targets/filter_openssl_privatekey_info/meta/main.yml @@ -0,0 +1,9 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +dependencies: + - setup_openssl + - setup_remote_tmp_dir + - prepare_jinja2_compat diff --git a/tests/integration/targets/filter_openssl_privatekey_info/tasks/impl.yml b/tests/integration/targets/filter_openssl_privatekey_info/tasks/impl.yml new file mode 100644 index 000000000..9985d7f15 --- /dev/null +++ b/tests/integration/targets/filter_openssl_privatekey_info/tasks/impl.yml @@ -0,0 +1,113 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +- name: Get key 1 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_1.pem') | community.crypto.openssl_privatekey_info }} + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' not in result" + +- name: Get key 2 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_2.pem') | community.crypto.openssl_privatekey_info(return_private_key_data=true) }} + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "result.public_data.size == default_rsa_key_size" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' in result" + - "result.public_data.modulus == result.private_data.p * result.private_data.q" + - "result.private_data.exponent > 5" + +- name: Get key 3 info (without passphrase) + set_fact: + result_: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_3.pem') | community.crypto.openssl_privatekey_info(return_private_key_data=true) }} + ignore_errors: yes + register: result + +- name: Check that loading passphrase protected key without passphrase failed + assert: + that: + - result is failed + - result.msg == 'Wrong or empty passphrase provided for private key' + +- name: Get key 3 info (with passphrase) + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_3.pem') | community.crypto.openssl_privatekey_info(passphrase='hunter2', return_private_key_data=true) }} + +- name: Check that RSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'RSA'" + - "'public_data' in result" + - "2 ** (result.public_data.size - 1) < result.public_data.modulus < 2 ** result.public_data.size" + - "result.public_data.exponent > 5" + - "'private_data' in result" + - "result.public_data.modulus == result.private_data.p * result.private_data.q" + - "result.private_data.exponent > 5" + +- name: Get key 4 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_4.pem') | community.crypto.openssl_privatekey_info(return_private_key_data=true) }} + +- name: Check that ECC key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'ECC'" + - "'public_data' in result" + - "result.public_data.curve is string" + - "result.public_data.x != 0" + - "result.public_data.y != 0" + - "result.public_data.exponent_size == (521 if (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') else 256)" + - "'private_data' in result" + - "result.private_data.multiplier > 1024" + +- name: Get key 5 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/privatekey_5.pem') | community.crypto.openssl_privatekey_info(return_private_key_data=true) }} + +- name: Check that DSA key info is ok + assert: + that: + - "'public_key' in result" + - "'public_key_fingerprints' in result" + - "'type' in result" + - "result.type == 'DSA'" + - "'public_data' in result" + - "result.public_data.p > 2" + - "result.public_data.q > 2" + - "result.public_data.g >= 2" + - "result.public_data.y > 2" + - "'private_data' in result" + - "result.private_data.x > 2" diff --git a/tests/integration/targets/filter_openssl_privatekey_info/tasks/main.yml b/tests/integration/targets/filter_openssl_privatekey_info/tasks/main.yml new file mode 100644 index 000000000..fcbd35971 --- /dev/null +++ b/tests/integration/targets/filter_openssl_privatekey_info/tasks/main.yml @@ -0,0 +1,43 @@ +--- +# Copyright (c) Ansible Project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Generate privatekey 1 + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_1.pem' + +- name: Generate privatekey 2 (less bits) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_2.pem' + type: RSA + size: '{{ default_rsa_key_size }}' + +- name: Generate privatekey 3 (with password) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_3.pem' + passphrase: hunter2 + cipher: auto + size: '{{ default_rsa_key_size }}' + +- name: Generate privatekey 4 (ECC) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_4.pem' + type: ECC + curve: "{{ (ansible_distribution == 'CentOS' and ansible_distribution_major_version == '6') | ternary('secp521r1', 'secp256k1') }}" + # ^ cryptography on CentOS6 doesn't support secp256k1, so we use secp521r1 instead + +- name: Generate privatekey 5 (DSA) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_5.pem' + type: DSA + size: 1024 + +- name: Running tests + include_tasks: impl.yml + when: cryptography_version.stdout is version('1.2.3', '>=')