1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2017, Thom Wiggers <ansible@thomwiggers.nl> 5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11DOCUMENTATION = r''' 12--- 13module: openssl_dhparam 14short_description: Generate OpenSSL Diffie-Hellman Parameters 15description: 16 - This module allows one to (re)generate OpenSSL DH-params. 17 - This module uses file common arguments to specify generated file permissions. 18 - "Please note that the module regenerates existing DH params if they do not 19 match the module's options. If you are concerned that this could overwrite 20 your existing DH params, consider using the I(backup) option." 21 - The module can use the cryptography Python library, or the C(openssl) executable. 22 By default, it tries to detect which one is available. This can be overridden 23 with the I(select_crypto_backend) option. 24requirements: 25 - Either cryptography >= 2.0 26 - Or OpenSSL binary C(openssl) 27author: 28 - Thom Wiggers (@thomwiggers) 29options: 30 state: 31 description: 32 - Whether the parameters should exist or not, 33 taking action if the state is different from what is stated. 34 type: str 35 default: present 36 choices: [ absent, present ] 37 size: 38 description: 39 - Size (in bits) of the generated DH-params. 40 type: int 41 default: 4096 42 force: 43 description: 44 - Should the parameters be regenerated even it it already exists. 45 type: bool 46 default: no 47 path: 48 description: 49 - Name of the file in which the generated parameters will be saved. 50 type: path 51 required: true 52 backup: 53 description: 54 - Create a backup file including a timestamp so you can get the original 55 DH params back if you overwrote them with new ones by accident. 56 type: bool 57 default: no 58 select_crypto_backend: 59 description: 60 - Determines which crypto backend to use. 61 - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(openssl). 62 - If set to C(openssl), will try to use the OpenSSL C(openssl) executable. 63 - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. 64 type: str 65 default: auto 66 choices: [ auto, cryptography, openssl ] 67 version_added: "1.0.0" 68 return_content: 69 description: 70 - If set to C(yes), will return the (current or generated) DH params' content as I(dhparams). 71 type: bool 72 default: no 73 version_added: "1.0.0" 74notes: 75- Supports C(check_mode). 76extends_documentation_fragment: 77- files 78seealso: 79- module: community.crypto.x509_certificate 80- module: community.crypto.openssl_csr 81- module: community.crypto.openssl_pkcs12 82- module: community.crypto.openssl_privatekey 83- module: community.crypto.openssl_publickey 84''' 85 86EXAMPLES = r''' 87- name: Generate Diffie-Hellman parameters with the default size (4096 bits) 88 community.crypto.openssl_dhparam: 89 path: /etc/ssl/dhparams.pem 90 91- name: Generate DH Parameters with a different size (2048 bits) 92 community.crypto.openssl_dhparam: 93 path: /etc/ssl/dhparams.pem 94 size: 2048 95 96- name: Force regenerate an DH parameters if they already exist 97 community.crypto.openssl_dhparam: 98 path: /etc/ssl/dhparams.pem 99 force: yes 100''' 101 102RETURN = r''' 103size: 104 description: Size (in bits) of the Diffie-Hellman parameters. 105 returned: changed or success 106 type: int 107 sample: 4096 108filename: 109 description: Path to the generated Diffie-Hellman parameters. 110 returned: changed or success 111 type: str 112 sample: /etc/ssl/dhparams.pem 113backup_file: 114 description: Name of backup file created. 115 returned: changed and if I(backup) is C(yes) 116 type: str 117 sample: /path/to/dhparams.pem.2019-03-09@11:22~ 118dhparams: 119 description: The (current or generated) DH params' content. 120 returned: if I(state) is C(present) and I(return_content) is C(yes) 121 type: str 122 version_added: "1.0.0" 123''' 124 125import abc 126import os 127import re 128import tempfile 129import traceback 130 131from distutils.version import LooseVersion 132 133from ansible.module_utils.basic import AnsibleModule, missing_required_lib 134from ansible.module_utils.common.text.converters import to_native 135 136from ansible_collections.community.crypto.plugins.module_utils.io import ( 137 load_file_if_exists, 138 write_file, 139) 140 141from ansible_collections.community.crypto.plugins.module_utils.crypto.math import ( 142 count_bits, 143) 144 145MINIMAL_CRYPTOGRAPHY_VERSION = '2.0' 146 147CRYPTOGRAPHY_IMP_ERR = None 148try: 149 import cryptography 150 import cryptography.exceptions 151 import cryptography.hazmat.backends 152 import cryptography.hazmat.primitives.asymmetric.dh 153 import cryptography.hazmat.primitives.serialization 154 CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) 155except ImportError: 156 CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() 157 CRYPTOGRAPHY_FOUND = False 158else: 159 CRYPTOGRAPHY_FOUND = True 160 161 162class DHParameterError(Exception): 163 pass 164 165 166class DHParameterBase(object): 167 168 def __init__(self, module): 169 self.state = module.params['state'] 170 self.path = module.params['path'] 171 self.size = module.params['size'] 172 self.force = module.params['force'] 173 self.changed = False 174 self.return_content = module.params['return_content'] 175 176 self.backup = module.params['backup'] 177 self.backup_file = None 178 179 @abc.abstractmethod 180 def _do_generate(self, module): 181 """Actually generate the DH params.""" 182 pass 183 184 def generate(self, module): 185 """Generate DH params.""" 186 changed = False 187 188 # ony generate when necessary 189 if self.force or not self._check_params_valid(module): 190 self._do_generate(module) 191 changed = True 192 193 # fix permissions (checking force not necessary as done above) 194 if not self._check_fs_attributes(module): 195 # Fix done implicitly by 196 # AnsibleModule.set_fs_attributes_if_different 197 changed = True 198 199 self.changed = changed 200 201 def remove(self, module): 202 if self.backup: 203 self.backup_file = module.backup_local(self.path) 204 try: 205 os.remove(self.path) 206 self.changed = True 207 except OSError as exc: 208 module.fail_json(msg=to_native(exc)) 209 210 def check(self, module): 211 """Ensure the resource is in its desired state.""" 212 if self.force: 213 return False 214 return self._check_params_valid(module) and self._check_fs_attributes(module) 215 216 @abc.abstractmethod 217 def _check_params_valid(self, module): 218 """Check if the params are in the correct state""" 219 pass 220 221 def _check_fs_attributes(self, module): 222 """Checks (and changes if not in check mode!) fs attributes""" 223 file_args = module.load_file_common_arguments(module.params) 224 if module.check_file_absent_if_check_mode(file_args['path']): 225 return False 226 return not module.set_fs_attributes_if_different(file_args, False) 227 228 def dump(self): 229 """Serialize the object into a dictionary.""" 230 231 result = { 232 'size': self.size, 233 'filename': self.path, 234 'changed': self.changed, 235 } 236 if self.backup_file: 237 result['backup_file'] = self.backup_file 238 if self.return_content: 239 content = load_file_if_exists(self.path, ignore_errors=True) 240 result['dhparams'] = content.decode('utf-8') if content else None 241 242 return result 243 244 245class DHParameterAbsent(DHParameterBase): 246 247 def __init__(self, module): 248 super(DHParameterAbsent, self).__init__(module) 249 250 def _do_generate(self, module): 251 """Actually generate the DH params.""" 252 pass 253 254 def _check_params_valid(self, module): 255 """Check if the params are in the correct state""" 256 pass 257 258 259class DHParameterOpenSSL(DHParameterBase): 260 261 def __init__(self, module): 262 super(DHParameterOpenSSL, self).__init__(module) 263 self.openssl_bin = module.get_bin_path('openssl', True) 264 265 def _do_generate(self, module): 266 """Actually generate the DH params.""" 267 # create a tempfile 268 fd, tmpsrc = tempfile.mkstemp() 269 os.close(fd) 270 module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit 271 # openssl dhparam -out <path> <bits> 272 command = [self.openssl_bin, 'dhparam', '-out', tmpsrc, str(self.size)] 273 rc, dummy, err = module.run_command(command, check_rc=False) 274 if rc != 0: 275 raise DHParameterError(to_native(err)) 276 if self.backup: 277 self.backup_file = module.backup_local(self.path) 278 try: 279 module.atomic_move(tmpsrc, self.path) 280 except Exception as e: 281 module.fail_json(msg="Failed to write to file %s: %s" % (self.path, str(e))) 282 283 def _check_params_valid(self, module): 284 """Check if the params are in the correct state""" 285 command = [self.openssl_bin, 'dhparam', '-check', '-text', '-noout', '-in', self.path] 286 rc, out, err = module.run_command(command, check_rc=False) 287 result = to_native(out) 288 if rc != 0: 289 # If the call failed the file probably doesn't exist or is 290 # unreadable 291 return False 292 # output contains "(xxxx bit)" 293 match = re.search(r"Parameters:\s+\((\d+) bit\).*", result) 294 if not match: 295 return False # No "xxxx bit" in output 296 297 bits = int(match.group(1)) 298 299 # if output contains "WARNING" we've got a problem 300 if "WARNING" in result or "WARNING" in to_native(err): 301 return False 302 303 return bits == self.size 304 305 306class DHParameterCryptography(DHParameterBase): 307 308 def __init__(self, module): 309 super(DHParameterCryptography, self).__init__(module) 310 self.crypto_backend = cryptography.hazmat.backends.default_backend() 311 312 def _do_generate(self, module): 313 """Actually generate the DH params.""" 314 # Generate parameters 315 params = cryptography.hazmat.primitives.asymmetric.dh.generate_parameters( 316 generator=2, 317 key_size=self.size, 318 backend=self.crypto_backend, 319 ) 320 # Serialize parameters 321 result = params.parameter_bytes( 322 encoding=cryptography.hazmat.primitives.serialization.Encoding.PEM, 323 format=cryptography.hazmat.primitives.serialization.ParameterFormat.PKCS3, 324 ) 325 # Write result 326 if self.backup: 327 self.backup_file = module.backup_local(self.path) 328 write_file(module, result) 329 330 def _check_params_valid(self, module): 331 """Check if the params are in the correct state""" 332 # Load parameters 333 try: 334 with open(self.path, 'rb') as f: 335 data = f.read() 336 params = self.crypto_backend.load_pem_parameters(data) 337 except Exception as dummy: 338 return False 339 # Check parameters 340 bits = count_bits(params.parameter_numbers().p) 341 return bits == self.size 342 343 344def main(): 345 """Main function""" 346 347 module = AnsibleModule( 348 argument_spec=dict( 349 state=dict(type='str', default='present', choices=['absent', 'present']), 350 size=dict(type='int', default=4096), 351 force=dict(type='bool', default=False), 352 path=dict(type='path', required=True), 353 backup=dict(type='bool', default=False), 354 select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'openssl']), 355 return_content=dict(type='bool', default=False), 356 ), 357 supports_check_mode=True, 358 add_file_common_args=True, 359 ) 360 361 base_dir = os.path.dirname(module.params['path']) or '.' 362 if not os.path.isdir(base_dir): 363 module.fail_json( 364 name=base_dir, 365 msg="The directory '%s' does not exist or the file is not a directory" % base_dir 366 ) 367 368 if module.params['state'] == 'present': 369 backend = module.params['select_crypto_backend'] 370 if backend == 'auto': 371 # Detection what is possible 372 can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) 373 can_use_openssl = module.get_bin_path('openssl', False) is not None 374 375 # First try cryptography, then OpenSSL 376 if can_use_cryptography: 377 backend = 'cryptography' 378 elif can_use_openssl: 379 backend = 'openssl' 380 381 # Success? 382 if backend == 'auto': 383 module.fail_json(msg=("Can't detect either the required Python library cryptography (>= {0}) " 384 "or the OpenSSL binary openssl").format(MINIMAL_CRYPTOGRAPHY_VERSION)) 385 386 if backend == 'openssl': 387 dhparam = DHParameterOpenSSL(module) 388 elif backend == 'cryptography': 389 if not CRYPTOGRAPHY_FOUND: 390 module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), 391 exception=CRYPTOGRAPHY_IMP_ERR) 392 dhparam = DHParameterCryptography(module) 393 394 if module.check_mode: 395 result = dhparam.dump() 396 result['changed'] = module.params['force'] or not dhparam.check(module) 397 module.exit_json(**result) 398 399 try: 400 dhparam.generate(module) 401 except DHParameterError as exc: 402 module.fail_json(msg=to_native(exc)) 403 else: 404 dhparam = DHParameterAbsent(module) 405 406 if module.check_mode: 407 result = dhparam.dump() 408 result['changed'] = os.path.exists(module.params['path']) 409 module.exit_json(**result) 410 411 if os.path.exists(module.params['path']): 412 try: 413 dhparam.remove(module) 414 except Exception as exc: 415 module.fail_json(msg=to_native(exc)) 416 417 result = dhparam.dump() 418 419 module.exit_json(**result) 420 421 422if __name__ == '__main__': 423 main() 424