Skip to content

Commit

Permalink
Add openssl_privatekey_info filter.
Browse files Browse the repository at this point in the history
  • Loading branch information
felixfontein committed Dec 29, 2022
1 parent c42733e commit 1f99f57
Show file tree
Hide file tree
Showing 7 changed files with 370 additions and 1 deletion.
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.
- "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,
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
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', '>=')

0 comments on commit 1f99f57

Please sign in to comment.