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

openssl_dhparam: add cryptography backend #62991

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- "openssl_dhparam - now supports a ``cryptography``-based backend. Auto-detection can be overwritten with the ``select_crypto_backend`` option."
20 changes: 20 additions & 0 deletions lib/ansible/module_utils/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
__metaclass__ = type


import sys
from distutils.version import LooseVersion

try:
Expand Down Expand Up @@ -1883,3 +1884,22 @@ def quick_is_not_prime(n):
# TODO: maybe do some iterations of Miller-Rabin to increase confidence
# (https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test)
return False


python_version = (sys.version_info[0], sys.version_info[1])
if python_version >= (2, 7) or python_version >= (3, 1):
# Ansible still supports Python 2.6 on remote nodes
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
def count_bits(no):
no = abs(no)
if no == 0:
return 0
return no.bit_length()
else:
# Slow, but works
def count_bits(no):
no = abs(no)
count = 0
while no > 0:
no >>= 1
count += 1
return count
211 changes: 171 additions & 40 deletions lib/ansible/modules/crypto/openssl_dhparam.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@
- "Please note that the module regenerates existing DH params if they don't
match the module's options. If you are concerned that this could overwrite
your existing DH params, consider using the I(backup) option."
- The module can use the cryptography Python library, or the C(openssl) executable.
By default, it tries to detect which one is available. This can be overridden
with the I(select_crypto_backend) option.
requirements:
- OpenSSL
- Either cryptography >= 2.0
- Or OpenSSL binary C(openssl)
author:
- Thom Wiggers (@thomwiggers)
- Thom Wiggers (@thomwiggers)
options:
state:
description:
Expand Down Expand Up @@ -56,6 +60,16 @@
type: bool
default: no
version_added: "2.8"
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(openssl).
- If set to C(openssl), will try to use the OpenSSL C(openssl) executable.
- If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [ auto, cryptography, openssl ]
version_added: '2.10'
extends_documentation_fragment:
- files
seealso:
Expand Down Expand Up @@ -100,52 +114,63 @@
sample: /path/to/dhparams.pem.2019-03-09@11:22~
'''

import abc
import os
import re
import tempfile
import traceback
from distutils.version import LooseVersion

from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_native
from ansible.module_utils import crypto as crypto_utils


MINIMAL_CRYPTOGRAPHY_VERSION = '2.0'

CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
import cryptography.exceptions
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.asymmetric.dh
import cryptography.hazmat.primitives.serialization
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
CRYPTOGRAPHY_FOUND = False
else:
CRYPTOGRAPHY_FOUND = True


class DHParameterError(Exception):
pass


class DHParameter(object):
class DHParameterBase(object):

def __init__(self, module):
self.state = module.params['state']
self.path = module.params['path']
self.size = module.params['size']
self.force = module.params['force']
self.changed = False
self.openssl_bin = module.get_bin_path('openssl', True)

self.backup = module.params['backup']
self.backup_file = None

@abc.abstractmethod
def _do_generate(self, module):
"""Actually generate the DH params."""
pass

def generate(self, module):
"""Generate a keypair."""
"""Generate DH params."""
changed = False

# ony generate when necessary
if self.force or not self._check_params_valid(module):
# create a tempfile
fd, tmpsrc = tempfile.mkstemp()
os.close(fd)
module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
# openssl dhparam -out <path> <bits>
command = [self.openssl_bin, 'dhparam', '-out', tmpsrc, str(self.size)]
rc, dummy, err = module.run_command(command, check_rc=False)
if rc != 0:
raise DHParameterError(to_native(err))
if self.backup:
self.backup_file = module.backup_local(self.path)
try:
module.atomic_move(tmpsrc, self.path)
except Exception as e:
module.fail_json(msg="Failed to write to file %s: %s" % (self.path, str(e)))
self._do_generate(module)
changed = True

# fix permissions (checking force not necessary as done above)
Expand All @@ -171,6 +196,70 @@ def check(self, module):
return False
return self._check_params_valid(module) and self._check_fs_attributes(module)

@abc.abstractmethod
def _check_params_valid(self, module):
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
"""Check if the params are in the correct state"""
pass

def _check_fs_attributes(self, module):
"""Checks (and changes if not in check mode!) fs attributes"""
file_args = module.load_file_common_arguments(module.params)
attrs_changed = module.set_fs_attributes_if_different(file_args, False)

return not attrs_changed

def dump(self):
"""Serialize the object into a dictionary."""

result = {
'size': self.size,
'filename': self.path,
'changed': self.changed,
}
if self.backup_file:
result['backup_file'] = self.backup_file

return result


class DHParameterAbsent(DHParameterBase):

def __init__(self, module):
super(DHParameterAbsent, self).__init__(module)

def _do_generate(self, module):
"""Actually generate the DH params."""
pass

def _check_params_valid(self, module):
"""Check if the params are in the correct state"""
pass


class DHParameterOpenSSL(DHParameterBase):

def __init__(self, module):
super(DHParameterOpenSSL, self).__init__(module)
self.openssl_bin = module.get_bin_path('openssl', True)

def _do_generate(self, module):
"""Actually generate the DH params."""
# create a tempfile
fd, tmpsrc = tempfile.mkstemp()
os.close(fd)
module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
# openssl dhparam -out <path> <bits>
command = [self.openssl_bin, 'dhparam', '-out', tmpsrc, str(self.size)]
rc, dummy, err = module.run_command(command, check_rc=False)
if rc != 0:
raise DHParameterError(to_native(err))
if self.backup:
self.backup_file = module.backup_local(self.path)
try:
module.atomic_move(tmpsrc, self.path)
except Exception as e:
module.fail_json(msg="Failed to write to file %s: %s" % (self.path, str(e)))

def _check_params_valid(self, module):
"""Check if the params are in the correct state"""
command = [self.openssl_bin, 'dhparam', '-check', '-text', '-noout', '-in', self.path]
Expand All @@ -193,25 +282,43 @@ def _check_params_valid(self, module):

return bits == self.size

def _check_fs_attributes(self, module):
"""Checks (and changes if not in check mode!) fs attributes"""
file_args = module.load_file_common_arguments(module.params)
attrs_changed = module.set_fs_attributes_if_different(file_args, False)

return not attrs_changed

def dump(self):
"""Serialize the object into a dictionary."""
class DHParameterCryptography(DHParameterBase):

result = {
'size': self.size,
'filename': self.path,
'changed': self.changed,
}
if self.backup_file:
result['backup_file'] = self.backup_file
def __init__(self, module):
super(DHParameterCryptography, self).__init__(module)
self.crypto_backend = cryptography.hazmat.backends.default_backend()

def _do_generate(self, module):
"""Actually generate the DH params."""
# Generate parameters
params = cryptography.hazmat.primitives.asymmetric.dh.generate_parameters(
generator=2,
key_size=self.size,
backend=self.crypto_backend,
)
# Serialize parameters
result = params.parameter_bytes(
encoding=cryptography.hazmat.primitives.serialization.Encoding.PEM,
format=cryptography.hazmat.primitives.serialization.ParameterFormat.PKCS3,
)
# Write result
if self.backup:
self.backup_file = module.backup_local(self.path)
crypto_utils.write_file(module, result)

return result
def _check_params_valid(self, module):
"""Check if the params are in the correct state"""
# Load parameters
try:
with open(self.path, 'rb') as f:
data = f.read()
params = self.crypto_backend.load_pem_parameters(data)
except Exception as dummy:
return False
# Check parameters
bits = crypto_utils.count_bits(params.parameter_numbers().p)
return bits == self.size


def main():
Expand All @@ -224,6 +331,7 @@ def main():
force=dict(type='bool', default=False),
path=dict(type='path', required=True),
backup=dict(type='bool', default=False),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'openssl']),
),
supports_check_mode=True,
add_file_common_args=True,
Expand All @@ -236,9 +344,31 @@ def main():
msg="The directory '%s' does not exist or the file is not a directory" % base_dir
)

dhparam = DHParameter(module)

if dhparam.state == 'present':
if module.params['state'] == 'present':
backend = module.params['select_crypto_backend']
if backend == 'auto':
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_openssl = module.get_bin_path('openssl', False) is not None

# First try cryptography, then OpenSSL
if can_use_cryptography:
backend = 'cryptography'
elif can_use_openssl:
backend = 'openssl'

# Success?
if backend == 'auto':
module.fail_json(msg=("Can't detect either the required Python library cryptography (>= {0}) "
"or the OpenSSL binary openssl").format(MINIMAL_CRYPTOGRAPHY_VERSION))

if backend == 'openssl':
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
dhparam = DHParameterOpenSSL(module)
elif backend == 'cryptography':
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
dhparam = DHParameterCryptography(module)

if module.check_mode:
result = dhparam.dump()
Expand All @@ -250,6 +380,7 @@ def main():
except DHParameterError as exc:
module.fail_json(msg=to_native(exc))
else:
dhparam = DHParameterAbsent(module)

if module.check_mode:
result = dhparam.dump()
Expand Down
2 changes: 2 additions & 0 deletions test/integration/targets/openssl_dhparam/meta/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dependencies:
- setup_openssl