diff --git a/plugins/filter/openssl_publickey_info.py b/plugins/filter/openssl_publickey_info.py new file mode 100644 index 000000000..f41af1c79 --- /dev/null +++ b/plugins/filter/openssl_publickey_info.py @@ -0,0 +1,162 @@ +# -*- 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_publickey_info +short_description: Retrieve information from OpenSSL public keys in PEM format +version_added: 2.10.0 +author: + - Felix Fontein (@felixfontein) +description: + - Provided a public key in OpenSSL PEM format, retrieve information. + - This is a filter version of the M(community.crypto.openssl_publickey_info) module. +options: + _input: + description: + - The content of the OpenSSL PEM public key. + type: string + required: true +seealso: + - module: community.crypto.openssl_publickey_info +''' + +EXAMPLES = ''' +- name: Show the type of a public key + ansible.builtin.debug: + msg: >- + {{ + ( + lookup('ansible.builtin.file', '/path/to/public-key.pem') + | community.crypto.openssl_publickey_info + ).type + }} +''' + +RETURN = ''' +_value: + description: + - Information on the public key. + type: dict + contains: + fingerprints: + description: + - Fingerprints of 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) +''' + +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.publickey_info import ( + PublicKeyParseError, + get_publickey_info, +) + +from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock + + +def openssl_publickey_info_filter(data): + '''Extract information from OpenSSL PEM public key.''' + if not isinstance(data, string_types): + raise AnsibleFilterError('The community.crypto.openssl_publickey_info input must be a text type, not %s' % type(data)) + + module = FilterModuleMock({}) + try: + return get_publickey_info(module, 'cryptography', content=to_bytes(data)) + except PublicKeyParseError 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_publickey_info': openssl_publickey_info_filter, + } diff --git a/plugins/modules/openssl_publickey_info.py b/plugins/modules/openssl_publickey_info.py index 611a905df..7b0610065 100644 --- a/plugins/modules/openssl_publickey_info.py +++ b/plugins/modules/openssl_publickey_info.py @@ -47,6 +47,10 @@ seealso: - module: community.crypto.openssl_publickey - module: community.crypto.openssl_privatekey_info + - ref: community.crypto.openssl_publickey_info filter + # - plugin: community.crypto.openssl_publickey_info + # plugin_type: filter + description: A filter variant of this module. ''' EXAMPLES = r''' diff --git a/tests/integration/targets/filter_openssl_publickey_info/aliases b/tests/integration/targets/filter_openssl_publickey_info/aliases new file mode 100644 index 000000000..4602f1185 --- /dev/null +++ b/tests/integration/targets/filter_openssl_publickey_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_publickey_info/meta/main.yml b/tests/integration/targets/filter_openssl_publickey_info/meta/main.yml new file mode 100644 index 000000000..7c2b42405 --- /dev/null +++ b/tests/integration/targets/filter_openssl_publickey_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_publickey_info/tasks/impl.yml b/tests/integration/targets/filter_openssl_publickey_info/tasks/impl.yml new file mode 100644 index 000000000..156f2a748 --- /dev/null +++ b/tests/integration/targets/filter_openssl_publickey_info/tasks/impl.yml @@ -0,0 +1,95 @@ +--- +# 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 ~ '/publickey_1.pem') | community.crypto.openssl_publickey_info }} + +- name: Check that RSA key info is ok + assert: + that: + - "'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" + +- name: Get key 2 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/publickey_2.pem') | community.crypto.openssl_publickey_info }} + +- name: Check that RSA key info is ok + assert: + that: + - "'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" + +- name: Get key 3 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/publickey_3.pem') | community.crypto.openssl_publickey_info }} + +- name: Check that ECC key info is ok + assert: + that: + - "'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)" + +- name: Get key 4 info + set_fact: + result: >- + {{ lookup('file', remote_tmp_dir ~ '/publickey_4.pem') | community.crypto.openssl_publickey_info }} + +- name: Check that DSA key info is ok + assert: + that: + - "'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" + +- name: Get invalid key info + set_fact: + result: >- + {{ [] | community.crypto.openssl_publickey_info }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - output.msg is search("^The community.crypto.openssl_publickey_info input must be a text type, not <(?:class|type) 'list'>$") + +- name: Get invalid key info + set_fact: + result: >- + {{ 'foo' | community.crypto.openssl_publickey_info }} + ignore_errors: true + register: output + +- name: Check that task failed and error message is OK + assert: + that: + - output is failed + - 'output.msg is search("^Error while deserializing key: ")' diff --git a/tests/integration/targets/filter_openssl_publickey_info/tasks/main.yml b/tests/integration/targets/filter_openssl_publickey_info/tasks/main.yml new file mode 100644 index 000000000..7375f45a6 --- /dev/null +++ b/tests/integration/targets/filter_openssl_publickey_info/tasks/main.yml @@ -0,0 +1,47 @@ +--- +# 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 (ECC) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_3.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 + select_crypto_backend: cryptography + +- name: Generate privatekey 4 (DSA) + openssl_privatekey: + path: '{{ remote_tmp_dir }}/privatekey_4.pem' + type: DSA + size: 1024 + +- name: Generate public keys + openssl_publickey: + privatekey_path: '{{ remote_tmp_dir }}/privatekey_{{ item }}.pem' + path: '{{ remote_tmp_dir }}/publickey_{{ item }}.pem' + loop: + - 1 + - 2 + - 3 + - 4 + +- name: Running tests + include_tasks: impl.yml + when: cryptography_version.stdout is version('1.2.3', '>=')