diff --git a/lib/ansible/modules/crypto/openssl_csr.py b/lib/ansible/modules/crypto/openssl_csr.py new file mode 100644 index 00000000000..eb8fe7ce403 --- /dev/null +++ b/lib/ansible/modules/crypto/openssl_csr.py @@ -0,0 +1,335 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# (c) 2017, Yanis Guenane +# +# 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 . + +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + +DOCUMENTATION = ''' +--- +module: openssl_csr +author: "Yanis Guenane (@Spredzy)" +version_added: "2.3" +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." +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 + 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 extention 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 + organizationUnitName: + required: false + aliases: [ 'OU' ] + description: + - organizationUnitName 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 Subject informations +- 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 + - success + type: string + sample: /etc/ssl/csr/www.ansible.com.csr +subject: + description: A dictionnary of the subject attached to the CSR + returned: + - changed + - success + type: list + sample: {'CN': 'www.ansible.com', 'O': 'Ansible'} +subjectAltName: + description: The alternative names this CSR is valid for + returned: + - changed + - success + type: string + sample: 'DNS:www.ansible.com,DNS:m.ansible.com' +''' + +from ansible.module_utils.basic import * + +try: + from OpenSSL import crypto +except ImportError: + pyopenssl_found = False +else: + pyopenssl_found = True + +import os + + +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.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'] + + for (key,value) in self.subject.items(): + if value is None: + del self.subject[key] + + 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("subjectAltName", False, self.subjectAltName)]) + + privatekey_content = open(self.privatekey_path).read() + self.privatekey = crypto.load_privatekey(crypto.FILETYPE_PEM, privatekey_content) + + req.set_pubkey(self.privatekey) + req.sign(self.privatekey, self.digest) + self.request = req + + try: + csr_file = open(self.path, 'w') + csr_file.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, self.request)) + csr_file.close() + except (IOError, OSError): + e = get_exception() + raise CertificateSigningRequestError(e) + 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: + e = get_exception() + if e.errno != errno.ENOENT: + raise CertificateSigningRequestError(e) + else: + self.changed = False + + + def dump(self): + '''Serialize the object into a dictionnary.''' + + 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'), + 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']], + ) + + 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: + e = get_exception() + module.fail_json(msg=str(e)) + + else: + + if module.check_mode: + result = csr.dump() + result['changed'] = os.path.exists(path) + module.exit_json(**result) + + try: + csr.remove() + except CertificateSigningRequestError: + e = get_exception() + module.fail_json(msg=str(e)) + + result = csr.dump() + + module.exit_json(**result) + + +if __name__ == "__main__": + main()