Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add openssl_privatekey_info filter #555

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions plugins/filter/openssl_privatekey_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# -*- coding: utf-8 -*-

# Copyright (c) 2022, Felix Fontein <[email protected]>
# 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.
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
- "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,
}
4 changes: 4 additions & 0 deletions plugins/modules/openssl_privatekey_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@
seealso:
- module: community.crypto.openssl_privatekey
- module: community.crypto.openssl_privatekey_pipe
- ref: community.crypto.openssl_privatekey_info filter <ansible_collections.community.crypto.openssl_privatekey_info_filter>
# - plugin: community.crypto.openssl_privatekey_info
# plugin_type: filter
description: A filter variant of this module.
'''

EXAMPLES = r'''
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -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', '>=')