community.general/lib/ansible/modules/crypto/openssl_csr.py
Yanis Guenane 8b22c45a45 Enable integration tests for the crypto/ namespace (#26684)
Crypto namespace contains the openssl modules. It has no integration
testing as of now.

This commits aims to add integration tests for the crypto namespace.
This will make it easier to spot breaking changes in the future.

This tests currently apply to:

  * openssl_privatekey
  * openssl_publickey
  * openssl_csr
2017-07-25 12:18:18 +01:00

346 lines
11 KiB
Python

#!/usr/bin/python
# -*- coding: utf-8 -*-
#
# (c) 2017, Yanis Guenane <yanis+ansible@guenane.org>
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
ANSIBLE_METADATA = {'metadata_version': '1.0',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: openssl_csr
author: "Yanis Guenane (@Spredzy)"
version_added: "2.4"
short_description: Generate OpenSSL Certificate Signing Request (CSR)
description:
- "This module allows one to (re)generates OpenSSL certificate signing requests.
It uses the pyOpenSSL python library to interact with openssl. This module support
the subjectAltName extension. Note: At least one of commonName or subjectAltName must
be specified. This module uses file common arguments to specify generated file permissions."
requirements:
- "python-pyOpenSSL"
options:
state:
required: false
default: "present"
choices: [ present, absent ]
description:
- Whether the certificate signing request should exist or not, taking action if the state is different from what is stated.
digest:
required: false
default: "sha256"
description:
- Digest used when signing the certificate signing request with the private key
privatekey_path:
required: true
description:
- Path to the privatekey to use when signing the certificate signing request
privatekey_passphrase:
required: false
description:
- The passphrase for the privatekey.
version_added: "2.4"
version:
required: false
default: 3
description:
- Version of the certificate signing request
force:
required: false
default: False
choices: [ True, False ]
description:
- Should the certificate signing request be forced regenerated by this ansible module
path:
required: true
description:
- Name of the folder in which the generated OpenSSL certificate signing request will be written
subjectAltName:
required: false
description:
- SAN extension to attach to the certificate signing request
countryName:
required: false
aliases: [ 'C' ]
description:
- countryName field of the certificate signing request subject
stateOrProvinceName:
required: false
aliases: [ 'ST' ]
description:
- stateOrProvinceName field of the certificate signing request subject
localityName:
required: false
aliases: [ 'L' ]
description:
- localityName field of the certificate signing request subject
organizationName:
required: false
aliases: [ 'O' ]
description:
- organizationName field of the certificate signing request subject
organizationalUnitName:
required: false
aliases: [ 'OU' ]
description:
- organizationalUnitName field of the certificate signing request subject
commonName:
required: false
aliases: [ 'CN' ]
description:
- commonName field of the certificate signing request subject
emailAddress:
required: false
aliases: [ 'E' ]
description:
- emailAddress field of the certificate signing request subject
'''
EXAMPLES = '''
# Generate an OpenSSL Certificate Signing Request
- openssl_csr:
path: /etc/ssl/csr/www.ansible.com.csr
privatekey_path: /etc/ssl/private/ansible.com.pem
commonName: www.ansible.com
# Generate an OpenSSL Certificate Signing Request with a
# passphrase protected private key
- openssl_csr:
path: /etc/ssl/csr/www.ansible.com.csr
privatekey_path: /etc/ssl/private/ansible.com.pem
privatekey_passphrase: ansible
commonName: www.ansible.com
# Generate an OpenSSL Certificate Signing Request with Subject information
- openssl_csr:
path: /etc/ssl/csr/www.ansible.com.csr
privatekey_path: /etc/ssl/private/ansible.com.pem
countryName: FR
organizationName: Ansible
emailAddress: jdoe@ansible.com
commonName: www.ansible.com
# Generate an OpenSSL Certificate Signing Request with subjectAltName extension
- openssl_csr:
path: /etc/ssl/csr/www.ansible.com.csr
privatekey_path: /etc/ssl/private/ansible.com.pem
subjectAltName: 'DNS:www.ansible.com,DNS:m.ansible.com'
# Force re-generate an OpenSSL Certificate Signing Request
- openssl_csr:
path: /etc/ssl/csr/www.ansible.com.csr
privatekey_path: /etc/ssl/private/ansible.com.pem
force: True
commonName: www.ansible.com
'''
RETURN = '''
csr:
description: Path to the generated Certificate Signing Request
returned: changed or success
type: string
sample: /etc/ssl/csr/www.ansible.com.csr
subject:
description: A dictionnary of the subject attached to the CSR
returned: changed or success
type: list
sample: {'CN': 'www.ansible.com', 'O': 'Ansible'}
subjectAltName:
description: The alternative names this CSR is valid for
returned: changed or success
type: string
sample: 'DNS:www.ansible.com,DNS:m.ansible.com'
'''
import errno
import os
try:
from OpenSSL import crypto
except ImportError:
pyopenssl_found = False
else:
pyopenssl_found = True
from ansible.module_utils import crypto as crypto_utils
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils._text import to_native
class CertificateSigningRequestError(Exception):
pass
class CertificateSigningRequest(object):
def __init__(self, module):
self.state = module.params['state']
self.digest = module.params['digest']
self.force = module.params['force']
self.subjectAltName = module.params['subjectAltName']
self.path = module.params['path']
self.privatekey_path = module.params['privatekey_path']
self.privatekey_passphrase = module.params['privatekey_passphrase']
self.version = module.params['version']
self.changed = True
self.request = None
self.privatekey = None
self.subject = {
'C': module.params['countryName'],
'ST': module.params['stateOrProvinceName'],
'L': module.params['localityName'],
'O': module.params['organizationName'],
'OU': module.params['organizationalUnitName'],
'CN': module.params['commonName'],
'emailAddress': module.params['emailAddress'],
}
if self.subjectAltName is None:
self.subjectAltName = 'DNS:%s' % self.subject['CN']
self.subject = dict((k, v) for k, v in self.subject.items() if v)
def generate(self, module):
'''Generate the certificate signing request.'''
if not os.path.exists(self.path) or self.force:
req = crypto.X509Req()
req.set_version(self.version)
subject = req.get_subject()
for (key, value) in self.subject.items():
if value is not None:
setattr(subject, key, value)
if self.subjectAltName is not None:
req.add_extensions([crypto.X509Extension(b"subjectAltName", False, self.subjectAltName.encode('ascii'))])
self.privatekey = crypto_utils.load_privatekey(
self.privatekey_path,
self.privatekey_passphrase
)
req.set_pubkey(self.privatekey)
req.sign(self.privatekey, self.digest)
self.request = req
try:
csr_file = open(self.path, 'wb')
csr_file.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, self.request))
csr_file.close()
except (IOError, OSError) as exc:
raise CertificateSigningRequestError(exc)
else:
self.changed = False
file_args = module.load_file_common_arguments(module.params)
if module.set_fs_attributes_if_different(file_args, False):
self.changed = True
def remove(self):
'''Remove the Certificate Signing Request.'''
try:
os.remove(self.path)
except OSError as exc:
if exc.errno != errno.ENOENT:
raise CertificateSigningRequestError(exc)
else:
self.changed = False
def dump(self):
'''Serialize the object into a dictionary.'''
result = {
'csr': self.path,
'subject': self.subject,
'subjectAltName': self.subjectAltName,
'changed': self.changed
}
return result
def main():
module = AnsibleModule(
argument_spec=dict(
state=dict(default='present', choices=['present', 'absent'], type='str'),
digest=dict(default='sha256', type='str'),
privatekey_path=dict(require=True, type='path'),
privatekey_passphrase=dict(type='str', no_log=True),
version=dict(default='3', type='int'),
force=dict(default=False, type='bool'),
subjectAltName=dict(aliases=['subjectAltName'], type='str'),
path=dict(required=True, type='path'),
countryName=dict(aliases=['C'], type='str'),
stateOrProvinceName=dict(aliases=['ST'], type='str'),
localityName=dict(aliases=['L'], type='str'),
organizationName=dict(aliases=['O'], type='str'),
organizationalUnitName=dict(aliases=['OU'], type='str'),
commonName=dict(aliases=['CN'], type='str'),
emailAddress=dict(aliases=['E'], type='str'),
),
add_file_common_args=True,
supports_check_mode=True,
required_one_of=[['commonName', 'subjectAltName']],
)
if not pyopenssl_found:
module.fail_json(msg='the python pyOpenSSL module is required')
path = module.params['path']
base_dir = os.path.dirname(module.params['path'])
if not os.path.isdir(base_dir):
module.fail_json(name=path, msg='The directory %s does not exist' % path)
csr = CertificateSigningRequest(module)
if module.params['state'] == 'present':
if module.check_mode:
result = csr.dump()
result['changed'] = module.params['force'] or not os.path.exists(path)
module.exit_json(**result)
try:
csr.generate(module)
except CertificateSigningRequestError as exc:
module.fail_json(msg=to_native(exc))
else:
if module.check_mode:
result = csr.dump()
result['changed'] = os.path.exists(path)
module.exit_json(**result)
try:
csr.remove()
except CertificateSigningRequestError as exc:
module.fail_json(msg=to_native(exc))
result = csr.dump()
module.exit_json(**result)
if __name__ == "__main__":
main()