diff options
author | Ansible Core Team <info@ansible.com> | 2020-03-09 09:40:30 +0000 |
---|---|---|
committer | Ansible Core Team <info@ansible.com> | 2020-03-09 09:40:30 +0000 |
commit | ef24d794eedb4b947bcbaa2681c7fc9cdfe8ff23 (patch) | |
tree | f02583d93085b8bfc9343dd60937b395ebb92254 /lib/ansible/modules | |
parent | 9d2d1370382f0790b0d9049640e19781497e1456 (diff) | |
download | ansible-ef24d794eedb4b947bcbaa2681c7fc9cdfe8ff23.tar.gz |
Migrated to community.crypto
Diffstat (limited to 'lib/ansible/modules')
25 files changed, 0 insertions, 16115 deletions
diff --git a/lib/ansible/modules/crypto/acme/_acme_account_facts.py b/lib/ansible/modules/crypto/acme/_acme_account_facts.py deleted file mode 120000 index ffd88bceb5..0000000000 --- a/lib/ansible/modules/crypto/acme/_acme_account_facts.py +++ /dev/null @@ -1 +0,0 @@ -acme_account_info.py
\ No newline at end of file diff --git a/lib/ansible/modules/crypto/acme/acme_account.py b/lib/ansible/modules/crypto/acme/acme_account.py deleted file mode 100644 index ec922ce693..0000000000 --- a/lib/ansible/modules/crypto/acme/acme_account.py +++ /dev/null @@ -1,278 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: acme_account -author: "Felix Fontein (@felixfontein)" -version_added: "2.6" -short_description: Create, modify or delete ACME accounts -description: - - "Allows to create, modify or delete accounts with a CA supporting the - L(ACME protocol,https://tools.ietf.org/html/rfc8555), - such as L(Let's Encrypt,https://letsencrypt.org/)." - - "This module only works with the ACME v2 protocol." -notes: - - "The M(acme_certificate) module also allows to do basic account management. - When using both modules, it is recommended to disable account management - for M(acme_certificate). For that, use the C(modify_account) option of - M(acme_certificate)." -seealso: - - name: Automatic Certificate Management Environment (ACME) - description: The specification of the ACME protocol (RFC 8555). - link: https://tools.ietf.org/html/rfc8555 - - module: acme_account_info - description: Retrieves facts about an ACME account. - - module: openssl_privatekey - description: Can be used to create a private account key. - - module: acme_inspect - description: Allows to debug problems. -extends_documentation_fragment: - - acme -options: - state: - description: - - "The state of the account, to be identified by its account key." - - "If the state is C(absent), the account will either not exist or be - deactivated." - - "If the state is C(changed_key), the account must exist. The account - key will be changed; no other information will be touched." - type: str - required: true - choices: - - present - - absent - - changed_key - allow_creation: - description: - - "Whether account creation is allowed (when state is C(present))." - type: bool - default: yes - contact: - description: - - "A list of contact URLs." - - "Email addresses must be prefixed with C(mailto:)." - - "See U(https://tools.ietf.org/html/rfc8555#section-7.3) - for what is allowed." - - "Must be specified when state is C(present). Will be ignored - if state is C(absent) or C(changed_key)." - type: list - elements: str - default: [] - terms_agreed: - description: - - "Boolean indicating whether you agree to the terms of service document." - - "ACME servers can require this to be true." - type: bool - default: no - new_account_key_src: - description: - - "Path to a file containing the ACME account RSA or Elliptic Curve key to change to." - - "Same restrictions apply as to C(account_key_src)." - - "Mutually exclusive with C(new_account_key_content)." - - "Required if C(new_account_key_content) is not used and state is C(changed_key)." - type: path - new_account_key_content: - description: - - "Content of the ACME account RSA or Elliptic Curve key to change to." - - "Same restrictions apply as to C(account_key_content)." - - "Mutually exclusive with C(new_account_key_src)." - - "Required if C(new_account_key_src) is not used and state is C(changed_key)." - type: str -''' - -EXAMPLES = ''' -- name: Make sure account exists and has given contacts. We agree to TOS. - acme_account: - account_key_src: /etc/pki/cert/private/account.key - state: present - terms_agreed: yes - contact: - - mailto:me@example.com - - mailto:myself@example.org - -- name: Make sure account has given email address. Don't create account if it doesn't exist - acme_account: - account_key_src: /etc/pki/cert/private/account.key - state: present - allow_creation: no - contact: - - mailto:me@example.com - -- name: Change account's key to the one stored in the variable new_account_key - acme_account: - account_key_src: /etc/pki/cert/private/account.key - new_account_key_content: '{{ new_account_key }}' - state: changed_key - -- name: Delete account (we have to use the new key) - acme_account: - account_key_content: '{{ new_account_key }}' - state: absent -''' - -RETURN = ''' -account_uri: - description: ACME account URI, or None if account does not exist. - returned: always - type: str -''' - -from ansible.module_utils.acme import ( - ModuleFailException, - ACMEAccount, - handle_standard_module_arguments, - get_default_argspec, -) - -from ansible.module_utils.basic import AnsibleModule - - -def main(): - argument_spec = get_default_argspec() - argument_spec.update(dict( - terms_agreed=dict(type='bool', default=False), - state=dict(type='str', required=True, choices=['absent', 'present', 'changed_key']), - allow_creation=dict(type='bool', default=True), - contact=dict(type='list', elements='str', default=[]), - new_account_key_src=dict(type='path'), - new_account_key_content=dict(type='str', no_log=True), - )) - module = AnsibleModule( - argument_spec=argument_spec, - required_one_of=( - ['account_key_src', 'account_key_content'], - ), - mutually_exclusive=( - ['account_key_src', 'account_key_content'], - ['new_account_key_src', 'new_account_key_content'], - ), - required_if=( - # Make sure that for state == changed_key, one of - # new_account_key_src and new_account_key_content are specified - ['state', 'changed_key', ['new_account_key_src', 'new_account_key_content'], True], - ), - supports_check_mode=True, - ) - handle_standard_module_arguments(module, needs_acme_v2=True) - - try: - account = ACMEAccount(module) - changed = False - state = module.params.get('state') - diff_before = {} - diff_after = {} - if state == 'absent': - created, account_data = account.setup_account(allow_creation=False) - if account_data: - diff_before = dict(account_data) - diff_before['public_account_key'] = account.key_data['jwk'] - if created: - raise AssertionError('Unwanted account creation') - if account_data is not None: - # Account is not yet deactivated - if not module.check_mode: - # Deactivate it - payload = { - 'status': 'deactivated' - } - result, info = account.send_signed_request(account.uri, payload) - if info['status'] != 200: - raise ModuleFailException('Error deactivating account: {0} {1}'.format(info['status'], result)) - changed = True - elif state == 'present': - allow_creation = module.params.get('allow_creation') - # Make sure contact is a list of strings (unfortunately, Ansible doesn't do that for us) - contact = [str(v) for v in module.params.get('contact')] - terms_agreed = module.params.get('terms_agreed') - created, account_data = account.setup_account( - contact, - terms_agreed=terms_agreed, - allow_creation=allow_creation, - ) - if account_data is None: - raise ModuleFailException(msg='Account does not exist or is deactivated.') - if created: - diff_before = {} - else: - diff_before = dict(account_data) - diff_before['public_account_key'] = account.key_data['jwk'] - updated = False - if not created: - updated, account_data = account.update_account(account_data, contact) - changed = created or updated - diff_after = dict(account_data) - diff_after['public_account_key'] = account.key_data['jwk'] - elif state == 'changed_key': - # Parse new account key - error, new_key_data = account.parse_key( - module.params.get('new_account_key_src'), - module.params.get('new_account_key_content') - ) - if error: - raise ModuleFailException("error while parsing account key: %s" % error) - # Verify that the account exists and has not been deactivated - created, account_data = account.setup_account(allow_creation=False) - if created: - raise AssertionError('Unwanted account creation') - if account_data is None: - raise ModuleFailException(msg='Account does not exist or is deactivated.') - diff_before = dict(account_data) - diff_before['public_account_key'] = account.key_data['jwk'] - # Now we can start the account key rollover - if not module.check_mode: - # Compose inner signed message - # https://tools.ietf.org/html/rfc8555#section-7.3.5 - url = account.directory['keyChange'] - protected = { - "alg": new_key_data['alg'], - "jwk": new_key_data['jwk'], - "url": url, - } - payload = { - "account": account.uri, - "newKey": new_key_data['jwk'], # specified in draft 12 and older - "oldKey": account.jwk, # specified in draft 13 and newer - } - data = account.sign_request(protected, payload, new_key_data) - # Send request and verify result - result, info = account.send_signed_request(url, data) - if info['status'] != 200: - raise ModuleFailException('Error account key rollover: {0} {1}'.format(info['status'], result)) - if module._diff: - account.key_data = new_key_data - account.jws_header['alg'] = new_key_data['alg'] - diff_after = account.get_account_data() - elif module._diff: - # Kind of fake diff_after - diff_after = dict(diff_before) - diff_after['public_account_key'] = new_key_data['jwk'] - changed = True - result = { - 'changed': changed, - 'account_uri': account.uri, - } - if module._diff: - result['diff'] = { - 'before': diff_before, - 'after': diff_after, - } - module.exit_json(**result) - except ModuleFailException as e: - e.do_fail(module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/acme/acme_account_info.py b/lib/ansible/modules/crypto/acme/acme_account_info.py deleted file mode 100644 index f60eb42a29..0000000000 --- a/lib/ansible/modules/crypto/acme/acme_account_info.py +++ /dev/null @@ -1,301 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2018 Felix Fontein <felix@fontein.de> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: acme_account_info -author: "Felix Fontein (@felixfontein)" -version_added: "2.7" -short_description: Retrieves information on ACME accounts -description: - - "Allows to retrieve information on accounts a CA supporting the - L(ACME protocol,https://tools.ietf.org/html/rfc8555), - such as L(Let's Encrypt,https://letsencrypt.org/)." - - "This module only works with the ACME v2 protocol." -notes: - - "The M(acme_account) module allows to modify, create and delete ACME accounts." - - "This module was called C(acme_account_facts) before Ansible 2.8. The usage - did not change." -options: - retrieve_orders: - description: - - "Whether to retrieve the list of order URLs or order objects, if provided - by the ACME server." - - "A value of C(ignore) will not fetch the list of orders." - - "Currently, Let's Encrypt does not return orders, so the C(orders) result - will always be empty." - type: str - choices: - - ignore - - url_list - - object_list - default: ignore - version_added: "2.9" -seealso: - - module: acme_account - description: Allows to create, modify or delete an ACME account. -extends_documentation_fragment: - - acme -''' - -EXAMPLES = ''' -- name: Check whether an account with the given account key exists - acme_account_info: - account_key_src: /etc/pki/cert/private/account.key - register: account_data -- name: Verify that account exists - assert: - that: - - account_data.exists -- name: Print account URI - debug: var=account_data.account_uri -- name: Print account contacts - debug: var=account_data.account.contact - -- name: Check whether the account exists and is accessible with the given account key - acme_account_info: - account_key_content: "{{ acme_account_key }}" - account_uri: "{{ acme_account_uri }}" - register: account_data -- name: Verify that account exists - assert: - that: - - account_data.exists -- name: Print account contacts - debug: var=account_data.account.contact -''' - -RETURN = ''' -exists: - description: Whether the account exists. - returned: always - type: bool - -account_uri: - description: ACME account URI, or None if account does not exist. - returned: always - type: str - -account: - description: The account information, as retrieved from the ACME server. - returned: if account exists - type: dict - contains: - contact: - description: the challenge resource that must be created for validation - returned: always - type: list - elements: str - sample: "['mailto:me@example.com', 'tel:00123456789']" - status: - description: the account's status - returned: always - type: str - choices: ['valid', 'deactivated', 'revoked'] - sample: valid - orders: - description: - - A URL where a list of orders can be retrieved for this account. - - Use the I(retrieve_orders) option to query this URL and retrieve the - complete list of orders. - returned: always - type: str - sample: https://example.ca/account/1/orders - public_account_key: - description: the public account key as a L(JSON Web Key,https://tools.ietf.org/html/rfc7517). - returned: always - type: str - sample: '{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"}' - -orders: - description: - - "The list of orders." - - "If I(retrieve_orders) is C(url_list), this will be a list of URLs." - - "If I(retrieve_orders) is C(object_list), this will be a list of objects." - type: list - #elements: ... depends on retrieve_orders - returned: if account exists, I(retrieve_orders) is not C(ignore), and server supports order listing - contains: - status: - description: The order's status. - type: str - choices: - - pending - - ready - - processing - - valid - - invalid - expires: - description: - - When the order expires. - - Timestamp should be formatted as described in RFC3339. - - Only required to be included in result when I(status) is C(pending) or C(valid). - type: str - returned: when server gives expiry date - identifiers: - description: - - List of identifiers this order is for. - type: list - elements: dict - contains: - type: - description: Type of identifier. C(dns) or C(ip). - type: str - value: - description: Name of identifier. Hostname or IP address. - type: str - wildcard: - description: "Whether I(value) is actually a wildcard. The wildcard - prefix C(*.) is not included in I(value) if this is C(true)." - type: bool - returned: required to be included if the identifier is wildcarded - notBefore: - description: - - The requested value of the C(notBefore) field in the certificate. - - Date should be formatted as described in RFC3339. - - Server is not required to return this. - type: str - returned: when server returns this - notAfter: - description: - - The requested value of the C(notAfter) field in the certificate. - - Date should be formatted as described in RFC3339. - - Server is not required to return this. - type: str - returned: when server returns this - error: - description: - - In case an error occurred during processing, this contains information about the error. - - The field is structured as a problem document (RFC7807). - type: dict - returned: when an error occurred - authorizations: - description: - - A list of URLs for authorizations for this order. - type: list - elements: str - finalize: - description: - - A URL used for finalizing an ACME order. - type: str - certificate: - description: - - The URL for retrieving the certificate. - type: str - returned: when certificate was issued -''' - -from ansible.module_utils.acme import ( - ModuleFailException, - ACMEAccount, - handle_standard_module_arguments, - process_links, - get_default_argspec, -) - -from ansible.module_utils.basic import AnsibleModule - - -def get_orders_list(module, account, orders_url): - ''' - Retrieves orders list (handles pagination). - ''' - orders = [] - while orders_url: - # Get part of orders list - res, info = account.get_request(orders_url, parse_json_result=True, fail_on_error=True) - if not res.get('orders'): - if orders: - module.warn('When retrieving orders list part {0}, got empty result list'.format(orders_url)) - break - # Add order URLs to result list - orders.extend(res['orders']) - # Extract URL of next part of results list - new_orders_url = [] - - def f(link, relation): - if relation == 'next': - new_orders_url.append(link) - - process_links(info, f) - new_orders_url.append(None) - previous_orders_url, orders_url = orders_url, new_orders_url.pop(0) - if orders_url == previous_orders_url: - # Prevent infinite loop - orders_url = None - return orders - - -def get_order(account, order_url): - ''' - Retrieve order data. - ''' - return account.get_request(order_url, parse_json_result=True, fail_on_error=True)[0] - - -def main(): - argument_spec = get_default_argspec() - argument_spec.update(dict( - retrieve_orders=dict(type='str', default='ignore', choices=['ignore', 'url_list', 'object_list']), - )) - module = AnsibleModule( - argument_spec=argument_spec, - required_one_of=( - ['account_key_src', 'account_key_content'], - ), - mutually_exclusive=( - ['account_key_src', 'account_key_content'], - ), - supports_check_mode=True, - ) - if module._name == 'acme_account_facts': - module.deprecate("The 'acme_account_facts' module has been renamed to 'acme_account_info'", version='2.12') - handle_standard_module_arguments(module, needs_acme_v2=True) - - try: - account = ACMEAccount(module) - # Check whether account exists - created, account_data = account.setup_account( - [], - allow_creation=False, - remove_account_uri_if_not_exists=True, - ) - if created: - raise AssertionError('Unwanted account creation') - result = { - 'changed': False, - 'exists': account.uri is not None, - 'account_uri': account.uri, - } - if account.uri is not None: - # Make sure promised data is there - if 'contact' not in account_data: - account_data['contact'] = [] - account_data['public_account_key'] = account.key_data['jwk'] - result['account'] = account_data - # Retrieve orders list - if account_data.get('orders') and module.params['retrieve_orders'] != 'ignore': - orders = get_orders_list(module, account, account_data['orders']) - if module.params['retrieve_orders'] == 'url_list': - result['orders'] = orders - else: - result['orders'] = [get_order(account, order) for order in orders] - module.exit_json(**result) - except ModuleFailException as e: - e.do_fail(module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/acme/acme_certificate.py b/lib/ansible/modules/crypto/acme/acme_certificate.py deleted file mode 100644 index fe0f32b653..0000000000 --- a/lib/ansible/modules/crypto/acme/acme_certificate.py +++ /dev/null @@ -1,1265 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: acme_certificate -author: "Michael Gruener (@mgruener)" -version_added: "2.2" -short_description: Create SSL/TLS certificates with the ACME protocol -description: - - "Create and renew SSL/TLS certificates with a CA supporting the - L(ACME protocol,https://tools.ietf.org/html/rfc8555), - such as L(Let's Encrypt,https://letsencrypt.org/) or - L(Buypass,https://www.buypass.com/). The current implementation - supports the C(http-01), C(dns-01) and C(tls-alpn-01) challenges." - - "To use this module, it has to be executed twice. Either as two - different tasks in the same run or during two runs. Note that the output - of the first run needs to be recorded and passed to the second run as the - module argument C(data)." - - "Between these two tasks you have to fulfill the required steps for the - chosen challenge by whatever means necessary. For C(http-01) that means - creating the necessary challenge file on the destination webserver. For - C(dns-01) the necessary dns record has to be created. For C(tls-alpn-01) - the necessary certificate has to be created and served. - It is I(not) the responsibility of this module to perform these steps." - - "For details on how to fulfill these challenges, you might have to read through - L(the main ACME specification,https://tools.ietf.org/html/rfc8555#section-8) - and the L(TLS-ALPN-01 specification,https://www.rfc-editor.org/rfc/rfc8737.html#section-3). - Also, consider the examples provided for this module." - - "The module includes experimental support for IP identifiers according to - the L(RFC 8738,https://www.rfc-editor.org/rfc/rfc8738.html)." -notes: - - "At least one of C(dest) and C(fullchain_dest) must be specified." - - "This module includes basic account management functionality. - If you want to have more control over your ACME account, use the M(acme_account) - module and disable account management for this module using the C(modify_account) - option." - - "This module was called C(letsencrypt) before Ansible 2.6. The usage - did not change." -seealso: - - name: The Let's Encrypt documentation - description: Documentation for the Let's Encrypt Certification Authority. - Provides useful information for example on rate limits. - link: https://letsencrypt.org/docs/ - - name: Buypass Go SSL - description: Documentation for the Buypass Certification Authority. - Provides useful information for example on rate limits. - link: https://www.buypass.com/ssl/products/acme - - name: Automatic Certificate Management Environment (ACME) - description: The specification of the ACME protocol (RFC 8555). - link: https://tools.ietf.org/html/rfc8555 - - name: ACME TLS ALPN Challenge Extension - description: The specification of the C(tls-alpn-01) challenge (RFC 8737). - link: https://www.rfc-editor.org/rfc/rfc8737.html-05 - - module: acme_challenge_cert_helper - description: Helps preparing C(tls-alpn-01) challenges. - - module: openssl_privatekey - description: Can be used to create private keys (both for certificates and accounts). - - module: openssl_csr - description: Can be used to create a Certificate Signing Request (CSR). - - module: certificate_complete_chain - description: Allows to find the root certificate for the returned fullchain. - - module: acme_certificate_revoke - description: Allows to revoke certificates. - - module: acme_account - description: Allows to create, modify or delete an ACME account. - - module: acme_inspect - description: Allows to debug problems. -extends_documentation_fragment: - - acme -options: - account_email: - description: - - "The email address associated with this account." - - "It will be used for certificate expiration warnings." - - "Note that when C(modify_account) is not set to C(no) and you also - used the M(acme_account) module to specify more than one contact - for your account, this module will update your account and restrict - it to the (at most one) contact email address specified here." - type: str - agreement: - description: - - "URI to a terms of service document you agree to when using the - ACME v1 service at C(acme_directory)." - - Default is latest gathered from C(acme_directory) URL. - - This option will only be used when C(acme_version) is 1. - type: str - terms_agreed: - description: - - "Boolean indicating whether you agree to the terms of service document." - - "ACME servers can require this to be true." - - This option will only be used when C(acme_version) is not 1. - type: bool - default: no - version_added: "2.5" - modify_account: - description: - - "Boolean indicating whether the module should create the account if - necessary, and update its contact data." - - "Set to C(no) if you want to use the M(acme_account) module to manage - your account instead, and to avoid accidental creation of a new account - using an old key if you changed the account key with M(acme_account)." - - "If set to C(no), C(terms_agreed) and C(account_email) are ignored." - type: bool - default: yes - version_added: "2.6" - challenge: - description: The challenge to be performed. - type: str - default: 'http-01' - choices: [ 'http-01', 'dns-01', 'tls-alpn-01' ] - csr: - description: - - "File containing the CSR for the new certificate." - - "Can be created with C(openssl req ...)." - - "The CSR may contain multiple Subject Alternate Names, but each one - will lead to an individual challenge that must be fulfilled for the - CSR to be signed." - - "I(Note): the private key used to create the CSR I(must not) be the - account key. This is a bad idea from a security point of view, and - the CA should not accept the CSR. The ACME server should return an - error in this case." - type: path - required: true - aliases: ['src'] - data: - description: - - "The data to validate ongoing challenges. This must be specified for - the second run of the module only." - - "The value that must be used here will be provided by a previous use - of this module. See the examples for more details." - - "Note that for ACME v2, only the C(order_uri) entry of C(data) will - be used. For ACME v1, C(data) must be non-empty to indicate the - second stage is active; all needed data will be taken from the - CSR." - - "I(Note): the C(data) option was marked as C(no_log) up to - Ansible 2.5. From Ansible 2.6 on, it is no longer marked this way - as it causes error messages to be come unusable, and C(data) does - not contain any information which can be used without having - access to the account key or which are not public anyway." - type: dict - dest: - description: - - "The destination file for the certificate." - - "Required if C(fullchain_dest) is not specified." - type: path - aliases: ['cert'] - fullchain_dest: - description: - - "The destination file for the full chain (i.e. certificate followed - by chain of intermediate certificates)." - - "Required if C(dest) is not specified." - type: path - version_added: 2.5 - aliases: ['fullchain'] - chain_dest: - description: - - If specified, the intermediate certificate will be written to this file. - type: path - version_added: 2.5 - aliases: ['chain'] - remaining_days: - description: - - "The number of days the certificate must have left being valid. - If C(cert_days < remaining_days), then it will be renewed. - If the certificate is not renewed, module return values will not - include C(challenge_data)." - - "To make sure that the certificate is renewed in any case, you can - use the C(force) option." - type: int - default: 10 - deactivate_authzs: - description: - - "Deactivate authentication objects (authz) after issuing a certificate, - or when issuing the certificate failed." - - "Authentication objects are bound to an account key and remain valid - for a certain amount of time, and can be used to issue certificates - without having to re-authenticate the domain. This can be a security - concern." - type: bool - default: no - version_added: 2.6 - force: - description: - - Enforces the execution of the challenge and validation, even if an - existing certificate is still valid for more than C(remaining_days). - - This is especially helpful when having an updated CSR e.g. with - additional domains for which a new certificate is desired. - type: bool - default: no - version_added: 2.6 - retrieve_all_alternates: - description: - - "When set to C(yes), will retrieve all alternate trust chains offered by the ACME CA. - These will not be written to disk, but will be returned together with the main - chain as C(all_chains). See the documentation for the C(all_chains) return - value for details." - type: bool - default: no - version_added: "2.9" - select_chain: - description: - - "Allows to specify criteria by which an (alternate) trust chain can be selected." - - "The list of criteria will be processed one by one until a chain is found - matching a criterium. If such a chain is found, it will be used by the - module instead of the default chain." - - "If a criterium matches multiple chains, the first one matching will be - returned. The order is determined by the ordering of the C(Link) headers - returned by the ACME server and might not be deterministic." - - "Every criterium can consist of multiple different conditions, like I(issuer) - and I(subject). For the criterium to match a chain, all conditions must apply - to the same certificate in the chain." - - "This option can only be used with the C(cryptography) backend." - type: list - version_added: "2.10" - suboptions: - test_certificates: - description: - - "Determines which certificates in the chain will be tested." - - "I(all) tests all certificates in the chain (excluding the leaf, which is - identical in all chains)." - - "I(last) only tests the last certificate in the chain, i.e. the one furthest - away from the leaf. Its issuer is the root certificate of this chain." - type: str - default: all - choices: [last, all] - issuer: - description: - - "Allows to specify parts of the issuer of a certificate in the chain must - have to be selected." - - "If I(issuer) is empty, any certificate will match." - - 'An example value would be C({"commonName": "My Preferred CA Root"}).' - type: dict - subject: - description: - - "Allows to specify parts of the subject of a certificate in the chain must - have to be selected." - - "If I(subject) is empty, any certificate will match." - - 'An example value would be C({"CN": "My Preferred CA Intermediate"})' - type: dict - subject_key_identifier: - description: - - "Checks for the SubjectKeyIdentifier extension. This is an identifier based - on the private key of the intermediate certificate." - - "The identifier must be of the form - C(A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1)." - type: str - authority_key_identifier: - description: - - "Checks for the AuthorityKeyIdentifier extension. This is an identifier based - on the private key of the issuer of the intermediate certificate." - - "The identifier must be of the form - C(C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10)." - type: str -''' - -EXAMPLES = r''' -### Example with HTTP challenge ### - -- name: Create a challenge for sample.com using a account key from a variable. - acme_certificate: - account_key_content: "{{ account_private_key }}" - csr: /etc/pki/cert/csr/sample.com.csr - dest: /etc/httpd/ssl/sample.com.crt - register: sample_com_challenge - -# Alternative first step: -- name: Create a challenge for sample.com using a account key from hashi vault. - acme_certificate: - account_key_content: "{{ lookup('hashi_vault', 'secret=secret/account_private_key:value') }}" - csr: /etc/pki/cert/csr/sample.com.csr - fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt - register: sample_com_challenge - -# Alternative first step: -- name: Create a challenge for sample.com using a account key file. - acme_certificate: - account_key_src: /etc/pki/cert/private/account.key - csr: /etc/pki/cert/csr/sample.com.csr - dest: /etc/httpd/ssl/sample.com.crt - fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt - register: sample_com_challenge - -# perform the necessary steps to fulfill the challenge -# for example: -# -# - copy: -# dest: /var/www/html/{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource'] }} -# content: "{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}" -# when: sample_com_challenge is changed - -- name: Let the challenge be validated and retrieve the cert and intermediate certificate - acme_certificate: - account_key_src: /etc/pki/cert/private/account.key - csr: /etc/pki/cert/csr/sample.com.csr - dest: /etc/httpd/ssl/sample.com.crt - fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt - chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt - data: "{{ sample_com_challenge }}" - -### Example with DNS challenge against production ACME server ### - -- name: Create a challenge for sample.com using a account key file. - acme_certificate: - account_key_src: /etc/pki/cert/private/account.key - account_email: myself@sample.com - src: /etc/pki/cert/csr/sample.com.csr - cert: /etc/httpd/ssl/sample.com.crt - challenge: dns-01 - acme_directory: https://acme-v01.api.letsencrypt.org/directory - # Renew if the certificate is at least 30 days old - remaining_days: 60 - register: sample_com_challenge - -# perform the necessary steps to fulfill the challenge -# for example: -# -# - route53: -# zone: sample.com -# record: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].record }}" -# type: TXT -# ttl: 60 -# state: present -# wait: yes -# # Note: route53 requires TXT entries to be enclosed in quotes -# value: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].resource_value | regex_replace('^(.*)$', '\"\\1\"') }}" -# when: sample_com_challenge is changed -# -# Alternative way: -# -# - route53: -# zone: sample.com -# record: "{{ item.key }}" -# type: TXT -# ttl: 60 -# state: present -# wait: yes -# # Note: item.value is a list of TXT entries, and route53 -# # requires every entry to be enclosed in quotes -# value: "{{ item.value | map('regex_replace', '^(.*)$', '\"\\1\"' ) | list }}" -# loop: "{{ sample_com_challenge.challenge_data_dns | dictsort }}" -# when: sample_com_challenge is changed - -- name: Let the challenge be validated and retrieve the cert and intermediate certificate - acme_certificate: - account_key_src: /etc/pki/cert/private/account.key - account_email: myself@sample.com - src: /etc/pki/cert/csr/sample.com.csr - cert: /etc/httpd/ssl/sample.com.crt - fullchain: /etc/httpd/ssl/sample.com-fullchain.crt - chain: /etc/httpd/ssl/sample.com-intermediate.crt - challenge: dns-01 - acme_directory: https://acme-v01.api.letsencrypt.org/directory - remaining_days: 60 - data: "{{ sample_com_challenge }}" - when: sample_com_challenge is changed - -# Alternative second step: -- name: Let the challenge be validated and retrieve the cert and intermediate certificate - acme_certificate: - account_key_src: /etc/pki/cert/private/account.key - account_email: myself@sample.com - src: /etc/pki/cert/csr/sample.com.csr - cert: /etc/httpd/ssl/sample.com.crt - fullchain: /etc/httpd/ssl/sample.com-fullchain.crt - chain: /etc/httpd/ssl/sample.com-intermediate.crt - challenge: tls-alpn-01 - remaining_days: 60 - data: "{{ sample_com_challenge }}" - # We use Let's Encrypt's ACME v2 endpoint - acme_directory: https://acme-v02.api.letsencrypt.org/directory - acme_version: 2 - # The following makes sure that if a chain with /CN=DST Root CA X3 in its issuer is provided - # as an alternative, it will be selected. These are the roots cross-signed by IdenTrust. - # As long as Let's Encrypt provides alternate chains with the cross-signed root(s) when - # switching to their own ISRG Root X1 root, this will use the chain ending with a cross-signed - # root. This chain is more compatible with older TLS clients. - select_chain: - - test_certificates: last - issuer: - CN: DST Root CA X3 - O: Digital Signature Trust Co. - when: sample_com_challenge is changed -''' - -RETURN = ''' -cert_days: - description: The number of days the certificate remains valid. - returned: success - type: int -challenge_data: - description: - - Per identifier / challenge type challenge data. - - Since Ansible 2.8.5, only challenges which are not yet valid are returned. - returned: changed - type: list - elements: dict - contains: - resource: - description: The challenge resource that must be created for validation. - returned: changed - type: str - sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA - resource_original: - description: - - The original challenge resource including type identifier for C(tls-alpn-01) - challenges. - returned: changed and challenge is C(tls-alpn-01) - type: str - sample: DNS:example.com - version_added: "2.8" - resource_value: - description: - - The value the resource has to produce for the validation. - - For C(http-01) and C(dns-01) challenges, the value can be used as-is. - - "For C(tls-alpn-01) challenges, note that this return value contains a - Base64 encoded version of the correct binary blob which has to be put - into the acmeValidation x509 extension; see - U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3) - for details. To do this, you might need the C(b64decode) Jinja filter - to extract the binary blob from this return value." - returned: changed - type: str - sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA - record: - description: The full DNS record's name for the challenge. - returned: changed and challenge is C(dns-01) - type: str - sample: _acme-challenge.example.com - version_added: "2.5" -challenge_data_dns: - description: - - List of TXT values per DNS record, in case challenge is C(dns-01). - - Since Ansible 2.8.5, only challenges which are not yet valid are returned. - returned: changed - type: dict - version_added: "2.5" -authorizations: - description: - - ACME authorization data. - - Maps an identifier to ACME authorization objects. See U(https://tools.ietf.org/html/rfc8555#section-7.1.4). - returned: changed - type: dict - sample: '{"example.com":{...}}' -order_uri: - description: ACME order URI. - returned: changed - type: str - version_added: "2.5" -finalization_uri: - description: ACME finalization URI. - returned: changed - type: str - version_added: "2.5" -account_uri: - description: ACME account URI. - returned: changed - type: str - version_added: "2.5" -all_chains: - description: - - When I(retrieve_all_alternates) is set to C(yes), the module will query the ACME server - for alternate chains. This return value will contain a list of all chains returned, - the first entry being the main chain returned by the server. - - See L(Section 7.4.2 of RFC8555,https://tools.ietf.org/html/rfc8555#section-7.4.2) for details. - returned: when certificate was retrieved and I(retrieve_all_alternates) is set to C(yes) - type: list - elements: dict - contains: - cert: - description: - - The leaf certificate itself, in PEM format. - type: str - returned: always - chain: - description: - - The certificate chain, excluding the root, as concatenated PEM certificates. - type: str - returned: always - full_chain: - description: - - The certificate chain, excluding the root, but including the leaf certificate, - as concatenated PEM certificates. - type: str - returned: always -''' - -from ansible.module_utils.acme import ( - ModuleFailException, - write_file, - nopad_b64, - pem_to_der, - ACMEAccount, - HAS_CURRENT_CRYPTOGRAPHY, - cryptography_get_csr_identifiers, - openssl_get_csr_identifiers, - cryptography_get_cert_days, - handle_standard_module_arguments, - process_links, - get_default_argspec, -) - -import base64 -import binascii -import hashlib -import os -import re -import textwrap -import time -import traceback -from datetime import datetime - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_bytes, to_native -from ansible.module_utils.compat import ipaddress as compat_ipaddress -from ansible.module_utils import crypto as crypto_utils - -try: - import cryptography - import cryptography.hazmat.backends - import cryptography.x509 -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - - -def get_cert_days(module, cert_file): - ''' - Return the days the certificate in cert_file remains valid and -1 - if the file was not found. If cert_file contains more than one - certificate, only the first one will be considered. - ''' - if HAS_CURRENT_CRYPTOGRAPHY: - return cryptography_get_cert_days(module, cert_file) - if not os.path.exists(cert_file): - return -1 - - openssl_bin = module.get_bin_path('openssl', True) - openssl_cert_cmd = [openssl_bin, "x509", "-in", cert_file, "-noout", "-text"] - dummy, out, dummy = module.run_command(openssl_cert_cmd, check_rc=True, encoding=None) - try: - not_after_str = re.search(r"\s+Not After\s*:\s+(.*)", out.decode('utf8')).group(1) - not_after = datetime.fromtimestamp(time.mktime(time.strptime(not_after_str, '%b %d %H:%M:%S %Y %Z'))) - except AttributeError: - raise ModuleFailException("No 'Not after' date found in {0}".format(cert_file)) - except ValueError: - raise ModuleFailException("Failed to parse 'Not after' date of {0}".format(cert_file)) - now = datetime.utcnow() - return (not_after - now).days - - -class ACMEClient(object): - ''' - ACME client class. Uses an ACME account object and a CSR to - start and validate ACME challenges and download the respective - certificates. - ''' - - def __init__(self, module): - self.module = module - self.version = module.params['acme_version'] - self.challenge = module.params['challenge'] - self.csr = module.params['csr'] - self.dest = module.params.get('dest') - self.fullchain_dest = module.params.get('fullchain_dest') - self.chain_dest = module.params.get('chain_dest') - self.account = ACMEAccount(module) - self.directory = self.account.directory - self.data = module.params['data'] - self.authorizations = None - self.cert_days = -1 - self.order_uri = self.data.get('order_uri') if self.data else None - self.finalize_uri = None - - # Make sure account exists - modify_account = module.params['modify_account'] - if modify_account or self.version > 1: - contact = [] - if module.params['account_email']: - contact.append('mailto:' + module.params['account_email']) - created, account_data = self.account.setup_account( - contact, - agreement=module.params.get('agreement'), - terms_agreed=module.params.get('terms_agreed'), - allow_creation=modify_account, - ) - if account_data is None: - raise ModuleFailException(msg='Account does not exist or is deactivated.') - updated = False - if not created and account_data and modify_account: - updated, account_data = self.account.update_account(account_data, contact) - self.changed = created or updated - else: - # This happens if modify_account is False and the ACME v1 - # protocol is used. In this case, we do not call setup_account() - # to avoid accidental creation of an account. This is OK - # since for ACME v1, the account URI is not needed to send a - # signed ACME request. - pass - - if not os.path.exists(self.csr): - raise ModuleFailException("CSR %s not found" % (self.csr)) - - self._openssl_bin = module.get_bin_path('openssl', True) - - # Extract list of identifiers from CSR - self.identifiers = self._get_csr_identifiers() - - def _get_csr_identifiers(self): - ''' - Parse the CSR and return the list of requested identifiers - ''' - if HAS_CURRENT_CRYPTOGRAPHY: - return cryptography_get_csr_identifiers(self.module, self.csr) - else: - return openssl_get_csr_identifiers(self._openssl_bin, self.module, self.csr) - - def _add_or_update_auth(self, identifier_type, identifier, auth): - ''' - Add or update the given authorization in the global authorizations list. - Return True if the auth was updated/added and False if no change was - necessary. - ''' - if self.authorizations.get(identifier_type + ':' + identifier) == auth: - return False - self.authorizations[identifier_type + ':' + identifier] = auth - return True - - def _new_authz_v1(self, identifier_type, identifier): - ''' - Create a new authorization for the given identifier. - Return the authorization object of the new authorization - https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4 - ''' - new_authz = { - "resource": "new-authz", - "identifier": {"type": identifier_type, "value": identifier}, - } - - result, info = self.account.send_signed_request(self.directory['new-authz'], new_authz) - if info['status'] not in [200, 201]: - raise ModuleFailException("Error requesting challenges: CODE: {0} RESULT: {1}".format(info['status'], result)) - else: - result['uri'] = info['location'] - return result - - def _get_challenge_data(self, auth, identifier_type, identifier): - ''' - Returns a dict with the data for all proposed (and supported) challenges - of the given authorization. - ''' - - data = {} - # no need to choose a specific challenge here as this module - # is not responsible for fulfilling the challenges. Calculate - # and return the required information for each challenge. - for challenge in auth['challenges']: - challenge_type = challenge['type'] - token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) - keyauthorization = self.account.get_keyauthorization(token) - - if challenge_type == 'http-01': - # https://tools.ietf.org/html/rfc8555#section-8.3 - resource = '.well-known/acme-challenge/' + token - data[challenge_type] = {'resource': resource, 'resource_value': keyauthorization} - elif challenge_type == 'dns-01': - if identifier_type != 'dns': - continue - # https://tools.ietf.org/html/rfc8555#section-8.4 - resource = '_acme-challenge' - value = nopad_b64(hashlib.sha256(to_bytes(keyauthorization)).digest()) - record = (resource + identifier[1:]) if identifier.startswith('*.') else (resource + '.' + identifier) - data[challenge_type] = {'resource': resource, 'resource_value': value, 'record': record} - elif challenge_type == 'tls-alpn-01': - # https://www.rfc-editor.org/rfc/rfc8737.html#section-3 - if identifier_type == 'ip': - # IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596) - resource = compat_ipaddress.ip_address(identifier).reverse_pointer - if not resource.endswith('.'): - resource += '.' - else: - resource = identifier - value = base64.b64encode(hashlib.sha256(to_bytes(keyauthorization)).digest()) - data[challenge_type] = {'resource': resource, 'resource_original': identifier_type + ':' + identifier, 'resource_value': value} - else: - continue - - return data - - def _fail_challenge(self, identifier_type, identifier, auth, error): - ''' - Aborts with a specific error for a challenge. - ''' - error_details = '' - # multiple challenges could have failed at this point, gather error - # details for all of them before failing - for challenge in auth['challenges']: - if challenge['status'] == 'invalid': - error_details += ' CHALLENGE: {0}'.format(challenge['type']) - if 'error' in challenge: - error_details += ' DETAILS: {0};'.format(challenge['error']['detail']) - else: - error_details += ';' - raise ModuleFailException("{0}: {1}".format(error.format(identifier_type + ':' + identifier), error_details)) - - def _validate_challenges(self, identifier_type, identifier, auth): - ''' - Validate the authorization provided in the auth dict. Returns True - when the validation was successful and False when it was not. - ''' - for challenge in auth['challenges']: - if self.challenge != challenge['type']: - continue - - uri = challenge['uri'] if self.version == 1 else challenge['url'] - - challenge_response = {} - if self.version == 1: - token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) - keyauthorization = self.account.get_keyauthorization(token) - challenge_response["resource"] = "challenge" - challenge_response["keyAuthorization"] = keyauthorization - challenge_response["type"] = self.challenge - result, info = self.account.send_signed_request(uri, challenge_response) - if info['status'] not in [200, 202]: - raise ModuleFailException("Error validating challenge: CODE: {0} RESULT: {1}".format(info['status'], result)) - - status = '' - - while status not in ['valid', 'invalid', 'revoked']: - result, dummy = self.account.get_request(auth['uri']) - result['uri'] = auth['uri'] - if self._add_or_update_auth(identifier_type, identifier, result): - self.changed = True - # https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2 - # "status (required, string): ... - # If this field is missing, then the default value is "pending"." - if self.version == 1 and 'status' not in result: - status = 'pending' - else: - status = result['status'] - time.sleep(2) - - if status == 'invalid': - self._fail_challenge(identifier_type, identifier, result, 'Authorization for {0} returned invalid') - - return status == 'valid' - - def _finalize_cert(self): - ''' - Create a new certificate based on the csr. - Return the certificate object as dict - https://tools.ietf.org/html/rfc8555#section-7.4 - ''' - csr = pem_to_der(self.csr) - new_cert = { - "csr": nopad_b64(csr), - } - result, info = self.account.send_signed_request(self.finalize_uri, new_cert) - if info['status'] not in [200]: - raise ModuleFailException("Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result)) - - status = result['status'] - while status not in ['valid', 'invalid']: - time.sleep(2) - result, dummy = self.account.get_request(self.order_uri) - status = result['status'] - - if status != 'valid': - raise ModuleFailException("Error new cert: CODE: {0} STATUS: {1} RESULT: {2}".format(info['status'], status, result)) - - return result['certificate'] - - def _der_to_pem(self, der_cert): - ''' - Convert the DER format certificate in der_cert to a PEM format - certificate and return it. - ''' - return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( - "\n".join(textwrap.wrap(base64.b64encode(der_cert).decode('utf8'), 64))) - - def _download_cert(self, url): - ''' - Download and parse the certificate chain. - https://tools.ietf.org/html/rfc8555#section-7.4.2 - ''' - content, info = self.account.get_request(url, parse_json_result=False, headers={'Accept': 'application/pem-certificate-chain'}) - - if not content or not info['content-type'].startswith('application/pem-certificate-chain'): - raise ModuleFailException("Cannot download certificate chain from {0}: {1} (headers: {2})".format(url, content, info)) - - cert = None - chain = [] - - # Parse data - lines = content.decode('utf-8').splitlines(True) - current = [] - for line in lines: - if line.strip(): - current.append(line) - if line.startswith('-----END CERTIFICATE-----'): - if cert is None: - cert = ''.join(current) - else: - chain.append(''.join(current)) - current = [] - - alternates = [] - - def f(link, relation): - if relation == 'up': - # Process link-up headers if there was no chain in reply - if not chain: - chain_result, chain_info = self.account.get_request(link, parse_json_result=False) - if chain_info['status'] in [200, 201]: - chain.append(self._der_to_pem(chain_result)) - elif relation == 'alternate': - alternates.append(link) - - process_links(info, f) - - if cert is None or current: - raise ModuleFailException("Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(url, content, info)) - return {'cert': cert, 'chain': chain, 'alternates': alternates} - - def _new_cert_v1(self): - ''' - Create a new certificate based on the CSR (ACME v1 protocol). - Return the certificate object as dict - https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5 - ''' - csr = pem_to_der(self.csr) - new_cert = { - "resource": "new-cert", - "csr": nopad_b64(csr), - } - result, info = self.account.send_signed_request(self.directory['new-cert'], new_cert) - - chain = [] - - def f(link, relation): - if relation == 'up': - chain_result, chain_info = self.account.get_request(link, parse_json_result=False) - if chain_info['status'] in [200, 201]: - del chain[:] - chain.append(self._der_to_pem(chain_result)) - - process_links(info, f) - - if info['status'] not in [200, 201]: - raise ModuleFailException("Error new cert: CODE: {0} RESULT: {1}".format(info['status'], result)) - else: - return {'cert': self._der_to_pem(result), 'uri': info['location'], 'chain': chain} - - def _new_order_v2(self): - ''' - Start a new certificate order (ACME v2 protocol). - https://tools.ietf.org/html/rfc8555#section-7.4 - ''' - identifiers = [] - for identifier_type, identifier in self.identifiers: - identifiers.append({ - 'type': identifier_type, - 'value': identifier, - }) - new_order = { - "identifiers": identifiers - } - result, info = self.account.send_signed_request(self.directory['newOrder'], new_order) - - if info['status'] not in [201]: - raise ModuleFailException("Error new order: CODE: {0} RESULT: {1}".format(info['status'], result)) - - for auth_uri in result['authorizations']: - auth_data, dummy = self.account.get_request(auth_uri) - auth_data['uri'] = auth_uri - identifier_type = auth_data['identifier']['type'] - identifier = auth_data['identifier']['value'] - if auth_data.get('wildcard', False): - identifier = '*.{0}'.format(identifier) - self.authorizations[identifier_type + ':' + identifier] = auth_data - - self.order_uri = info['location'] - self.finalize_uri = result['finalize'] - - def is_first_step(self): - ''' - Return True if this is the first execution of this module, i.e. if a - sufficient data object from a first run has not been provided. - ''' - if self.data is None: - return True - if self.version == 1: - # As soon as self.data is a non-empty object, we are in the second stage. - return not self.data - else: - # We are in the second stage if data.order_uri is given (which has been - # stored in self.order_uri by the constructor). - return self.order_uri is None - - def start_challenges(self): - ''' - Create new authorizations for all identifiers of the CSR, - respectively start a new order for ACME v2. - ''' - self.authorizations = {} - if self.version == 1: - for identifier_type, identifier in self.identifiers: - if identifier_type != 'dns': - raise ModuleFailException('ACME v1 only supports DNS identifiers!') - for identifier_type, identifier in self.identifiers: - new_auth = self._new_authz_v1(identifier_type, identifier) - self._add_or_update_auth(identifier_type, identifier, new_auth) - else: - self._new_order_v2() - self.changed = True - - def get_challenges_data(self): - ''' - Get challenge details for the chosen challenge type. - Return a tuple of generic challenge details, and specialized DNS challenge details. - ''' - # Get general challenge data - data = {} - for type_identifier, auth in self.authorizations.items(): - identifier_type, identifier = type_identifier.split(':', 1) - auth = self.authorizations[type_identifier] - # Skip valid authentications: their challenges are already valid - # and do not need to be returned - if auth['status'] == 'valid': - continue - # We drop the type from the key to preserve backwards compatibility - data[identifier] = self._get_challenge_data(auth, identifier_type, identifier) - # Get DNS challenge data - data_dns = {} - if self.challenge == 'dns-01': - for identifier, challenges in data.items(): - if self.challenge in challenges: - values = data_dns.get(challenges[self.challenge]['record']) - if values is None: - values = [] - data_dns[challenges[self.challenge]['record']] = values - values.append(challenges[self.challenge]['resource_value']) - return data, data_dns - - def finish_challenges(self): - ''' - Verify challenges for all identifiers of the CSR. - ''' - self.authorizations = {} - - # Step 1: obtain challenge information - if self.version == 1: - # For ACME v1, we attempt to create new authzs. Existing ones - # will be returned instead. - for identifier_type, identifier in self.identifiers: - new_auth = self._new_authz_v1(identifier_type, identifier) - self._add_or_update_auth(identifier_type, identifier, new_auth) - else: - # For ACME v2, we obtain the order object by fetching the - # order URI, and extract the information from there. - result, info = self.account.get_request(self.order_uri) - - if not result: - raise ModuleFailException("Cannot download order from {0}: {1} (headers: {2})".format(self.order_uri, result, info)) - - if info['status'] not in [200]: - raise ModuleFailException("Error on downloading order: CODE: {0} RESULT: {1}".format(info['status'], result)) - - for auth_uri in result['authorizations']: - auth_data, dummy = self.account.get_request(auth_uri) - auth_data['uri'] = auth_uri - identifier_type = auth_data['identifier']['type'] - identifier = auth_data['identifier']['value'] - if auth_data.get('wildcard', False): - identifier = '*.{0}'.format(identifier) - self.authorizations[identifier_type + ':' + identifier] = auth_data - - self.finalize_uri = result['finalize'] - - # Step 2: validate challenges - for type_identifier, auth in self.authorizations.items(): - if auth['status'] == 'pending': - identifier_type, identifier = type_identifier.split(':', 1) - self._validate_challenges(identifier_type, identifier, auth) - - def _chain_matches(self, chain, criterium): - ''' - Check whether an alternate chain matches the specified criterium. - ''' - if criterium['test_certificates'] == 'last': - chain = chain[-1:] - for cert in chain: - try: - x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography.hazmat.backends.default_backend()) - matches = True - if criterium['subject']: - for k, v in crypto_utils.parse_name_field(criterium['subject']): - oid = crypto_utils.cryptography_name_to_oid(k) - value = to_native(v) - found = False - for attribute in x509.subject: - if attribute.oid == oid and value == to_native(attribute.value): - found = True - break - if not found: - matches = False - break - if criterium['issuer']: - for k, v in crypto_utils.parse_name_field(criterium['issuer']): - oid = crypto_utils.cryptography_name_to_oid(k) - value = to_native(v) - found = False - for attribute in x509.issuer: - if attribute.oid == oid and value == to_native(attribute.value): - found = True - break - if not found: - matches = False - break - if criterium['subject_key_identifier']: - try: - ext = x509.extensions.get_extension_for_class(cryptography.x509.SubjectKeyIdentifier) - if criterium['subject_key_identifier'] != ext.value.digest: - matches = False - except cryptography.x509.ExtensionNotFound: - matches = False - if criterium['authority_key_identifier']: - try: - ext = x509.extensions.get_extension_for_class(cryptography.x509.AuthorityKeyIdentifier) - if criterium['authority_key_identifier'] != ext.value.key_identifier: - matches = False - except cryptography.x509.ExtensionNotFound: - matches = False - if matches: - return True - except Exception as e: - self.module.warn('Error while loading certificate {0}: {1}'.format(cert, e)) - return False - - def get_certificate(self): - ''' - Request a new certificate and write it to the destination file. - First verifies whether all authorizations are valid; if not, aborts - with an error. - ''' - for identifier_type, identifier in self.identifiers: - auth = self.authorizations.get(identifier_type + ':' + identifier) - if auth is None: - raise ModuleFailException('Found no authorization information for "{0}"!'.format(identifier_type + ':' + identifier)) - if 'status' not in auth: - self._fail_challenge(identifier_type, identifier, auth, 'Authorization for {0} returned no status') - if auth['status'] != 'valid': - self._fail_challenge(identifier_type, identifier, auth, 'Authorization for {0} returned status ' + str(auth['status'])) - - if self.version == 1: - cert = self._new_cert_v1() - else: - cert_uri = self._finalize_cert() - cert = self._download_cert(cert_uri) - if self.module.params['retrieve_all_alternates'] or self.module.params['select_chain']: - # Retrieve alternate chains - alternate_chains = [] - for alternate in cert['alternates']: - try: - alt_cert = self._download_cert(alternate) - except ModuleFailException as e: - self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e)) - continue - alternate_chains.append(alt_cert) - - # Prepare return value for all alternate chains - if self.module.params['retrieve_all_alternates']: - self.all_chains = [] - - def _append_all_chains(cert_data): - self.all_chains.append(dict( - cert=cert_data['cert'].encode('utf8'), - chain=("\n".join(cert_data.get('chain', []))).encode('utf8'), - full_chain=(cert_data['cert'] + "\n".join(cert_data.get('chain', []))).encode('utf8'), - )) - - _append_all_chains(cert) - for alt_chain in alternate_chains: - _append_all_chains(alt_chain) - - # Try to select alternate chain depending on criteria - if self.module.params['select_chain']: - matching_chain = None - all_chains = [cert] + alternate_chains - for criterium_idx, criterium in enumerate(self.module.params['select_chain']): - for v in ('subject_key_identifier', 'authority_key_identifier'): - if criterium[v]: - try: - criterium[v] = binascii.unhexlify(criterium[v].replace(':', '')) - except Exception: - self.module.warn('Criterium {0} in select_chain has invalid {1} value. ' - 'Ignoring criterium.'.format(criterium_idx, v)) - continue - for alt_chain in all_chains: - if self._chain_matches(alt_chain.get('chain', []), criterium): - self.module.debug('Found matching chain for criterium {0}'.format(criterium_idx)) - matching_chain = alt_chain - break - if matching_chain: - break - if matching_chain: - cert.update(matching_chain) - else: - self.module.debug('Found no matching alternative chain') - - if cert['cert'] is not None: - pem_cert = cert['cert'] - - chain = [link for link in cert.get('chain', [])] - - if self.dest and write_file(self.module, self.dest, pem_cert.encode('utf8')): - self.cert_days = get_cert_days(self.module, self.dest) - self.changed = True - - if self.fullchain_dest and write_file(self.module, self.fullchain_dest, (pem_cert + "\n".join(chain)).encode('utf8')): - self.cert_days = get_cert_days(self.module, self.fullchain_dest) - self.changed = True - - if self.chain_dest and write_file(self.module, self.chain_dest, ("\n".join(chain)).encode('utf8')): - self.changed = True - - def deactivate_authzs(self): - ''' - Deactivates all valid authz's. Does not raise exceptions. - https://community.letsencrypt.org/t/authorization-deactivation/19860/2 - https://tools.ietf.org/html/rfc8555#section-7.5.2 - ''' - authz_deactivate = { - 'status': 'deactivated' - } - if self.version == 1: - authz_deactivate['resource'] = 'authz' - if self.authorizations: - for identifier_type, identifier in self.identifiers: - auth = self.authorizations.get(identifier_type + ':' + identifier) - if auth is None or auth.get('status') != 'valid': - continue - try: - result, info = self.account.send_signed_request(auth['uri'], authz_deactivate) - if 200 <= info['status'] < 300 and result.get('status') == 'deactivated': - auth['status'] = 'deactivated' - except Exception as dummy: - # Ignore errors on deactivating authzs - pass - if auth.get('status') != 'deactivated': - self.module.warn(warning='Could not deactivate authz object {0}.'.format(auth['uri'])) - - -def main(): - argument_spec = get_default_argspec() - argument_spec.update(dict( - modify_account=dict(type='bool', default=True), - account_email=dict(type='str'), - agreement=dict(type='str'), - terms_agreed=dict(type='bool', default=False), - challenge=dict(type='str', default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01']), - csr=dict(type='path', required=True, aliases=['src']), - data=dict(type='dict'), - dest=dict(type='path', aliases=['cert']), - fullchain_dest=dict(type='path', aliases=['fullchain']), - chain_dest=dict(type='path', aliases=['chain']), - remaining_days=dict(type='int', default=10), - deactivate_authzs=dict(type='bool', default=False), - force=dict(type='bool', default=False), - retrieve_all_alternates=dict(type='bool', default=False), - select_chain=dict(type='list', elements='dict', options=dict( - test_certificates=dict(type='str', default='all', choices=['last', 'all']), - issuer=dict(type='dict'), - subject=dict(type='dict'), - subject_key_identifier=dict(type='str'), - authority_key_identifier=dict(type='str'), - )), - )) - module = AnsibleModule( - argument_spec=argument_spec, - required_one_of=( - ['account_key_src', 'account_key_content'], - ['dest', 'fullchain_dest'], - ), - mutually_exclusive=( - ['account_key_src', 'account_key_content'], - ), - supports_check_mode=True, - ) - backend = handle_standard_module_arguments(module) - if module.params['select_chain']: - if backend != 'cryptography': - module.fail_json(msg="The 'select_chain' can only be used with the 'cryptography' backend.") - elif not CRYPTOGRAPHY_FOUND: - module.fail_json(msg=missing_required_lib('cryptography')) - - try: - if module.params.get('dest'): - cert_days = get_cert_days(module, module.params['dest']) - else: - cert_days = get_cert_days(module, module.params['fullchain_dest']) - - if module.params['force'] or cert_days < module.params['remaining_days']: - # If checkmode is active, base the changed state solely on the status - # of the certificate file as all other actions (accessing an account, checking - # the authorization status...) would lead to potential changes of the current - # state - if module.check_mode: - module.exit_json(changed=True, authorizations={}, challenge_data={}, cert_days=cert_days) - else: - client = ACMEClient(module) - client.cert_days = cert_days - other = dict() - if client.is_first_step(): - # First run: start challenges / start new order - client.start_challenges() - else: - # Second run: finish challenges, and get certificate - try: - client.finish_challenges() - client.get_certificate() - if module.params['retrieve_all_alternates']: - other['all_chains'] = client.all_chains - finally: - if module.params['deactivate_authzs']: - client.deactivate_authzs() - data, data_dns = client.get_challenges_data() - auths = dict() - for k, v in client.authorizations.items(): - # Remove "type:" from key - auths[k.split(':', 1)[1]] = v - module.exit_json( - changed=client.changed, - authorizations=auths, - finalize_uri=client.finalize_uri, - order_uri=client.order_uri, - account_uri=client.account.uri, - challenge_data=data, - challenge_data_dns=data_dns, - cert_days=client.cert_days, - **other - ) - else: - module.exit_json(changed=False, cert_days=cert_days) - except ModuleFailException as e: - e.do_fail(module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/acme/acme_certificate_revoke.py b/lib/ansible/modules/crypto/acme/acme_certificate_revoke.py deleted file mode 100644 index b048e8e675..0000000000 --- a/lib/ansible/modules/crypto/acme/acme_certificate_revoke.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: acme_certificate_revoke -author: "Felix Fontein (@felixfontein)" -version_added: "2.7" -short_description: Revoke certificates with the ACME protocol -description: - - "Allows to revoke certificates issued by a CA supporting the - L(ACME protocol,https://tools.ietf.org/html/rfc8555), - such as L(Let's Encrypt,https://letsencrypt.org/)." -notes: - - "Exactly one of C(account_key_src), C(account_key_content), - C(private_key_src) or C(private_key_content) must be specified." - - "Trying to revoke an already revoked certificate - should result in an unchanged status, even if the revocation reason - was different than the one specified here. Also, depending on the - server, it can happen that some other error is returned if the - certificate has already been revoked." -seealso: - - name: The Let's Encrypt documentation - description: Documentation for the Let's Encrypt Certification Authority. - Provides useful information for example on rate limits. - link: https://letsencrypt.org/docs/ - - name: Automatic Certificate Management Environment (ACME) - description: The specification of the ACME protocol (RFC 8555). - link: https://tools.ietf.org/html/rfc8555 - - module: acme_inspect - description: Allows to debug problems. -extends_documentation_fragment: - - acme -options: - certificate: - description: - - "Path to the certificate to revoke." - type: path - required: yes - account_key_src: - description: - - "Path to a file containing the ACME account RSA or Elliptic Curve - key." - - "RSA keys can be created with C(openssl rsa ...). Elliptic curve keys can - be created with C(openssl ecparam -genkey ...). Any other tool creating - private keys in PEM format can be used as well." - - "Mutually exclusive with C(account_key_content)." - - "Required if C(account_key_content) is not used." - type: path - account_key_content: - description: - - "Content of the ACME account RSA or Elliptic Curve key." - - "Note that exactly one of C(account_key_src), C(account_key_content), - C(private_key_src) or C(private_key_content) must be specified." - - "I(Warning): the content will be written into a temporary file, which will - be deleted by Ansible when the module completes. Since this is an - important private key — it can be used to change the account key, - or to revoke your certificates without knowing their private keys - —, this might not be acceptable." - - "In case C(cryptography) is used, the content is not written into a - temporary file. It can still happen that it is written to disk by - Ansible in the process of moving the module with its argument to - the node where it is executed." - type: str - private_key_src: - description: - - "Path to the certificate's private key." - - "Note that exactly one of C(account_key_src), C(account_key_content), - C(private_key_src) or C(private_key_content) must be specified." - type: path - private_key_content: - description: - - "Content of the certificate's private key." - - "Note that exactly one of C(account_key_src), C(account_key_content), - C(private_key_src) or C(private_key_content) must be specified." - - "I(Warning): the content will be written into a temporary file, which will - be deleted by Ansible when the module completes. Since this is an - important private key — it can be used to change the account key, - or to revoke your certificates without knowing their private keys - —, this might not be acceptable." - - "In case C(cryptography) is used, the content is not written into a - temporary file. It can still happen that it is written to disk by - Ansible in the process of moving the module with its argument to - the node where it is executed." - type: str - revoke_reason: - description: - - "One of the revocation reasonCodes defined in - L(Section 5.3.1 of RFC5280,https://tools.ietf.org/html/rfc5280#section-5.3.1)." - - "Possible values are C(0) (unspecified), C(1) (keyCompromise), - C(2) (cACompromise), C(3) (affiliationChanged), C(4) (superseded), - C(5) (cessationOfOperation), C(6) (certificateHold), - C(8) (removeFromCRL), C(9) (privilegeWithdrawn), - C(10) (aACompromise)" - type: int -''' - -EXAMPLES = ''' -- name: Revoke certificate with account key - acme_certificate_revoke: - account_key_src: /etc/pki/cert/private/account.key - certificate: /etc/httpd/ssl/sample.com.crt - -- name: Revoke certificate with certificate's private key - acme_certificate_revoke: - private_key_src: /etc/httpd/ssl/sample.com.key - certificate: /etc/httpd/ssl/sample.com.crt -''' - -RETURN = ''' -''' - -from ansible.module_utils.acme import ( - ModuleFailException, - ACMEAccount, - nopad_b64, - pem_to_der, - handle_standard_module_arguments, - get_default_argspec, -) - -from ansible.module_utils.basic import AnsibleModule - - -def main(): - argument_spec = get_default_argspec() - argument_spec.update(dict( - private_key_src=dict(type='path'), - private_key_content=dict(type='str', no_log=True), - certificate=dict(type='path', required=True), - revoke_reason=dict(type='int'), - )) - module = AnsibleModule( - argument_spec=argument_spec, - required_one_of=( - ['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'], - ), - mutually_exclusive=( - ['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'], - ), - supports_check_mode=False, - ) - handle_standard_module_arguments(module) - - try: - account = ACMEAccount(module) - # Load certificate - certificate = pem_to_der(module.params.get('certificate')) - certificate = nopad_b64(certificate) - # Construct payload - payload = { - 'certificate': certificate - } - if module.params.get('revoke_reason') is not None: - payload['reason'] = module.params.get('revoke_reason') - # Determine endpoint - if module.params.get('acme_version') == 1: - endpoint = account.directory['revoke-cert'] - payload['resource'] = 'revoke-cert' - else: - endpoint = account.directory['revokeCert'] - # Get hold of private key (if available) and make sure it comes from disk - private_key = module.params.get('private_key_src') - private_key_content = module.params.get('private_key_content') - # Revoke certificate - if private_key or private_key_content: - # Step 1: load and parse private key - error, private_key_data = account.parse_key(private_key, private_key_content) - if error: - raise ModuleFailException("error while parsing private key: %s" % error) - # Step 2: sign revokation request with private key - jws_header = { - "alg": private_key_data['alg'], - "jwk": private_key_data['jwk'], - } - result, info = account.send_signed_request(endpoint, payload, key_data=private_key_data, jws_header=jws_header) - else: - # Step 1: get hold of account URI - created, account_data = account.setup_account(allow_creation=False) - if created: - raise AssertionError('Unwanted account creation') - if account_data is None: - raise ModuleFailException(msg='Account does not exist or is deactivated.') - # Step 2: sign revokation request with account key - result, info = account.send_signed_request(endpoint, payload) - if info['status'] != 200: - already_revoked = False - # Standardized error from draft 14 on (https://tools.ietf.org/html/rfc8555#section-7.6) - if result.get('type') == 'urn:ietf:params:acme:error:alreadyRevoked': - already_revoked = True - else: - # Hack for Boulder errors - if module.params.get('acme_version') == 1: - error_type = 'urn:acme:error:malformed' - else: - error_type = 'urn:ietf:params:acme:error:malformed' - if result.get('type') == error_type and result.get('detail') == 'Certificate already revoked': - # Fallback: boulder returns this in case the certificate was already revoked. - already_revoked = True - # If we know the certificate was already revoked, we don't fail, - # but successfully terminate while indicating no change - if already_revoked: - module.exit_json(changed=False) - raise ModuleFailException('Error revoking certificate: {0} {1}'.format(info['status'], result)) - module.exit_json(changed=True) - except ModuleFailException as e: - e.do_fail(module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/acme/acme_challenge_cert_helper.py b/lib/ansible/modules/crypto/acme/acme_challenge_cert_helper.py deleted file mode 100644 index 7a355cb1c1..0000000000 --- a/lib/ansible/modules/crypto/acme/acme_challenge_cert_helper.py +++ /dev/null @@ -1,299 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2018 Felix Fontein <felix@fontein.de> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: acme_challenge_cert_helper -author: "Felix Fontein (@felixfontein)" -version_added: "2.7" -short_description: Prepare certificates required for ACME challenges such as C(tls-alpn-01) -description: - - "Prepares certificates for ACME challenges such as C(tls-alpn-01)." - - "The raw data is provided by the M(acme_certificate) module, and needs to be - converted to a certificate to be used for challenge validation. This module - provides a simple way to generate the required certificates." -seealso: - - name: Automatic Certificate Management Environment (ACME) - description: The specification of the ACME protocol (RFC 8555). - link: https://tools.ietf.org/html/rfc8555 - - name: ACME TLS ALPN Challenge Extension - description: The specification of the C(tls-alpn-01) challenge (RFC 8737). - link: https://www.rfc-editor.org/rfc/rfc8737.html -requirements: - - "cryptography >= 1.3" -options: - challenge: - description: - - "The challenge type." - type: str - required: yes - choices: - - tls-alpn-01 - challenge_data: - description: - - "The C(challenge_data) entry provided by M(acme_certificate) for the challenge." - type: dict - required: yes - private_key_src: - description: - - "Path to a file containing the private key file to use for this challenge - certificate." - - "Mutually exclusive with C(private_key_content)." - type: path - private_key_content: - description: - - "Content of the private key to use for this challenge certificate." - - "Mutually exclusive with C(private_key_src)." - type: str -''' - -EXAMPLES = ''' -- name: Create challenges for a given CRT for sample.com - acme_certificate: - account_key_src: /etc/pki/cert/private/account.key - challenge: tls-alpn-01 - csr: /etc/pki/cert/csr/sample.com.csr - dest: /etc/httpd/ssl/sample.com.crt - register: sample_com_challenge - -- name: Create certificates for challenges - acme_challenge_cert_helper: - challenge: tls-alpn-01 - challenge_data: "{{ item.value['tls-alpn-01'] }}" - private_key_src: /etc/pki/cert/key/sample.com.key - loop: "{{ sample_com_challenge.challenge_data | dictsort }}" - register: sample_com_challenge_certs - -- name: Install challenge certificates - # We need to set up HTTPS such that for the domain, - # regular_certificate is delivered for regular connections, - # except if ALPN selects the "acme-tls/1"; then, the - # challenge_certificate must be delivered. - # This can for example be achieved with very new versions - # of NGINX; search for ssl_preread and - # ssl_preread_alpn_protocols for information on how to - # route by ALPN protocol. - ...: - domain: "{{ item.domain }}" - challenge_certificate: "{{ item.challenge_certificate }}" - regular_certificate: "{{ item.regular_certificate }}" - private_key: /etc/pki/cert/key/sample.com.key - loop: "{{ sample_com_challenge_certs.results }}" - -- name: Create certificate for a given CSR for sample.com - acme_certificate: - account_key_src: /etc/pki/cert/private/account.key - challenge: tls-alpn-01 - csr: /etc/pki/cert/csr/sample.com.csr - dest: /etc/httpd/ssl/sample.com.crt - data: "{{ sample_com_challenge }}" -''' - -RETURN = ''' -domain: - description: - - "The domain the challenge is for. The certificate should be provided if - this is specified in the request's the C(Host) header." - returned: always - type: str -identifier_type: - description: - - "The identifier type for the actual resource identifier. Will be C(dns) - or C(ip)." - returned: always - type: str - version_added: "2.8" -identifier: - description: - - "The identifier for the actual resource. Will be a domain name if the - type is C(dns), or an IP address if the type is C(ip)." - returned: always - type: str - version_added: "2.8" -challenge_certificate: - description: - - "The challenge certificate in PEM format." - returned: always - type: str -regular_certificate: - description: - - "A self-signed certificate for the challenge domain." - - "If no existing certificate exists, can be used to set-up - https in the first place if that is needed for providing - the challenge." - returned: always - type: str -''' - -from ansible.module_utils.acme import ( - ModuleFailException, - read_file, -) - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_bytes, to_text - -import base64 -import datetime -import sys -import traceback - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - import cryptography.hazmat.backends - import cryptography.hazmat.primitives.serialization - import cryptography.hazmat.primitives.asymmetric.rsa - import cryptography.hazmat.primitives.asymmetric.ec - import cryptography.hazmat.primitives.asymmetric.padding - import cryptography.hazmat.primitives.hashes - import cryptography.hazmat.primitives.asymmetric.utils - import cryptography.x509 - import cryptography.x509.oid - import ipaddress - from distutils.version import LooseVersion - HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.3')) - _cryptography_backend = cryptography.hazmat.backends.default_backend() -except ImportError as dummy: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - HAS_CRYPTOGRAPHY = False - - -# Convert byte string to ASN1 encoded octet string -if sys.version_info[0] >= 3: - def encode_octet_string(octet_string): - if len(octet_string) >= 128: - raise ModuleFailException('Cannot handle octet strings with more than 128 bytes') - return bytes([0x4, len(octet_string)]) + octet_string -else: - def encode_octet_string(octet_string): - if len(octet_string) >= 128: - raise ModuleFailException('Cannot handle octet strings with more than 128 bytes') - return b'\x04' + chr(len(octet_string)) + octet_string - - -def main(): - module = AnsibleModule( - argument_spec=dict( - challenge=dict(type='str', required=True, choices=['tls-alpn-01']), - challenge_data=dict(type='dict', required=True), - private_key_src=dict(type='path'), - private_key_content=dict(type='str', no_log=True), - ), - required_one_of=( - ['private_key_src', 'private_key_content'], - ), - mutually_exclusive=( - ['private_key_src', 'private_key_content'], - ), - ) - if not HAS_CRYPTOGRAPHY: - module.fail_json(msg=missing_required_lib('cryptography >= 1.3'), exception=CRYPTOGRAPHY_IMP_ERR) - - try: - # Get parameters - challenge = module.params['challenge'] - challenge_data = module.params['challenge_data'] - - # Get hold of private key - private_key_content = module.params.get('private_key_content') - if private_key_content is None: - private_key_content = read_file(module.params['private_key_src']) - else: - private_key_content = to_bytes(private_key_content) - try: - private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(private_key_content, password=None, backend=_cryptography_backend) - except Exception as e: - raise ModuleFailException('Error while loading private key: {0}'.format(e)) - - # Some common attributes - domain = to_text(challenge_data['resource']) - identifier_type, identifier = to_text(challenge_data.get('resource_original', 'dns:' + challenge_data['resource'])).split(':', 1) - subject = issuer = cryptography.x509.Name([]) - not_valid_before = datetime.datetime.utcnow() - not_valid_after = datetime.datetime.utcnow() + datetime.timedelta(days=10) - if identifier_type == 'dns': - san = cryptography.x509.DNSName(identifier) - elif identifier_type == 'ip': - san = cryptography.x509.IPAddress(ipaddress.ip_address(identifier)) - else: - raise ModuleFailException('Unsupported identifier type "{0}"'.format(identifier_type)) - - # Generate regular self-signed certificate - regular_certificate = cryptography.x509.CertificateBuilder().subject_name( - subject - ).issuer_name( - issuer - ).public_key( - private_key.public_key() - ).serial_number( - cryptography.x509.random_serial_number() - ).not_valid_before( - not_valid_before - ).not_valid_after( - not_valid_after - ).add_extension( - cryptography.x509.SubjectAlternativeName([san]), - critical=False, - ).sign( - private_key, - cryptography.hazmat.primitives.hashes.SHA256(), - _cryptography_backend - ) - - # Process challenge - if challenge == 'tls-alpn-01': - value = base64.b64decode(challenge_data['resource_value']) - challenge_certificate = cryptography.x509.CertificateBuilder().subject_name( - subject - ).issuer_name( - issuer - ).public_key( - private_key.public_key() - ).serial_number( - cryptography.x509.random_serial_number() - ).not_valid_before( - not_valid_before - ).not_valid_after( - not_valid_after - ).add_extension( - cryptography.x509.SubjectAlternativeName([san]), - critical=False, - ).add_extension( - cryptography.x509.UnrecognizedExtension( - cryptography.x509.ObjectIdentifier("1.3.6.1.5.5.7.1.31"), - encode_octet_string(value), - ), - critical=True, - ).sign( - private_key, - cryptography.hazmat.primitives.hashes.SHA256(), - _cryptography_backend - ) - - module.exit_json( - changed=True, - domain=domain, - identifier_type=identifier_type, - identifier=identifier, - challenge_certificate=challenge_certificate.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM), - regular_certificate=regular_certificate.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM) - ) - except ModuleFailException as e: - e.do_fail(module) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/acme/acme_inspect.py b/lib/ansible/modules/crypto/acme/acme_inspect.py deleted file mode 100644 index 05ff506b20..0000000000 --- a/lib/ansible/modules/crypto/acme/acme_inspect.py +++ /dev/null @@ -1,320 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2018 Felix Fontein (@felixfontein) -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = r''' ---- -module: acme_inspect -author: "Felix Fontein (@felixfontein)" -version_added: "2.8" -short_description: Send direct requests to an ACME server -description: - - "Allows to send direct requests to an ACME server with the - L(ACME protocol,https://tools.ietf.org/html/rfc8555), - which is supported by CAs such as L(Let's Encrypt,https://letsencrypt.org/)." - - "This module can be used to debug failed certificate request attempts, - for example when M(acme_certificate) fails or encounters a problem which - you wish to investigate." - - "The module can also be used to directly access features of an ACME servers - which are not yet supported by the Ansible ACME modules." -notes: - - "The I(account_uri) option must be specified for properly authenticated - ACME v2 requests (except a C(new-account) request)." - - "Using the C(ansible) tool, M(acme_inspect) can be used to directly execute - ACME requests without the need of writing a playbook. For example, the - following command retrieves the ACME account with ID 1 from Let's Encrypt - (assuming C(/path/to/key) is the correct private account key): - C(ansible localhost -m acme_inspect -a \"account_key_src=/path/to/key - acme_directory=https://acme-v02.api.letsencrypt.org/directory acme_version=2 - account_uri=https://acme-v02.api.letsencrypt.org/acme/acct/1 method=get - url=https://acme-v02.api.letsencrypt.org/acme/acct/1\")" -seealso: - - name: Automatic Certificate Management Environment (ACME) - description: The specification of the ACME protocol (RFC 8555). - link: https://tools.ietf.org/html/rfc8555 - - name: ACME TLS ALPN Challenge Extension - description: The specification of the C(tls-alpn-01) challenge (RFC 8737). - link: https://www.rfc-editor.org/rfc/rfc8737.html -extends_documentation_fragment: - - acme -options: - url: - description: - - "The URL to send the request to." - - "Must be specified if I(method) is not C(directory-only)." - type: str - method: - description: - - "The method to use to access the given URL on the ACME server." - - "The value C(post) executes an authenticated POST request. The content - must be specified in the I(content) option." - - "The value C(get) executes an authenticated POST-as-GET request for ACME v2, - and a regular GET request for ACME v1." - - "The value C(directory-only) only retrieves the directory, without doing - a request." - type: str - default: get - choices: - - get - - post - - directory-only - content: - description: - - "An encoded JSON object which will be sent as the content if I(method) - is C(post)." - - "Required when I(method) is C(post), and not allowed otherwise." - type: str - fail_on_acme_error: - description: - - "If I(method) is C(post) or C(get), make the module fail in case an ACME - error is returned." - type: bool - default: yes -''' - -EXAMPLES = r''' -- name: Get directory - acme_inspect: - acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory - acme_version: 2 - method: directory-only - register: directory - -- name: Create an account - acme_inspect: - acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory - acme_version: 2 - account_key_src: /etc/pki/cert/private/account.key - url: "{{ directory.newAccount}}" - method: post - content: '{"termsOfServiceAgreed":true}' - register: account_creation - # account_creation.headers.location contains the account URI - # if creation was successful - -- name: Get account information - acme_inspect: - acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory - acme_version: 2 - account_key_src: /etc/pki/cert/private/account.key - account_uri: "{{ account_creation.headers.location }}" - url: "{{ account_creation.headers.location }}" - method: get - -- name: Update account contacts - acme_inspect: - acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory - acme_version: 2 - account_key_src: /etc/pki/cert/private/account.key - account_uri: "{{ account_creation.headers.location }}" - url: "{{ account_creation.headers.location }}" - method: post - content: '{{ account_info | to_json }}' - vars: - account_info: - # For valid values, see - # https://tools.ietf.org/html/rfc8555#section-7.3 - contact: - - mailto:me@example.com - -- name: Create certificate order - acme_certificate: - acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory - acme_version: 2 - account_key_src: /etc/pki/cert/private/account.key - account_uri: "{{ account_creation.headers.location }}" - csr: /etc/pki/cert/csr/sample.com.csr - fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt - challenge: http-01 - register: certificate_request - -# Assume something went wrong. certificate_request.order_uri contains -# the order URI. - -- name: Get order information - acme_inspect: - acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory - acme_version: 2 - account_key_src: /etc/pki/cert/private/account.key - account_uri: "{{ account_creation.headers.location }}" - url: "{{ certificate_request.order_uri }}" - method: get - register: order - -- name: Get first authz for order - acme_inspect: - acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory - acme_version: 2 - account_key_src: /etc/pki/cert/private/account.key - account_uri: "{{ account_creation.headers.location }}" - url: "{{ order.output_json.authorizations[0] }}" - method: get - register: authz - -- name: Get HTTP-01 challenge for authz - acme_inspect: - acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory - acme_version: 2 - account_key_src: /etc/pki/cert/private/account.key - account_uri: "{{ account_creation.headers.location }}" - url: "{{ authz.output_json.challenges | selectattr('type', 'equalto', 'http-01') }}" - method: get - register: http01challenge - -- name: Activate HTTP-01 challenge manually - acme_inspect: - acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory - acme_version: 2 - account_key_src: /etc/pki/cert/private/account.key - account_uri: "{{ account_creation.headers.location }}" - url: "{{ http01challenge.url }}" - method: post - content: '{}' -''' - -RETURN = ''' -directory: - description: The ACME directory's content - returned: always - type: dict - sample: | - { - "a85k3x9f91A4": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417", - "keyChange": "https://acme-v02.api.letsencrypt.org/acme/key-change", - "meta": { - "caaIdentities": [ - "letsencrypt.org" - ], - "termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf", - "website": "https://letsencrypt.org" - }, - "newAccount": "https://acme-v02.api.letsencrypt.org/acme/new-acct", - "newNonce": "https://acme-v02.api.letsencrypt.org/acme/new-nonce", - "newOrder": "https://acme-v02.api.letsencrypt.org/acme/new-order", - "revokeCert": "https://acme-v02.api.letsencrypt.org/acme/revoke-cert" - } -headers: - description: The request's HTTP headers (with lowercase keys) - returned: always - type: dict - sample: | - { - "boulder-requester": "12345", - "cache-control": "max-age=0, no-cache, no-store", - "connection": "close", - "content-length": "904", - "content-type": "application/json", - "cookies": {}, - "cookies_string": "", - "date": "Wed, 07 Nov 2018 12:34:56 GMT", - "expires": "Wed, 07 Nov 2018 12:44:56 GMT", - "link": "<https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf>;rel=\"terms-of-service\"", - "msg": "OK (904 bytes)", - "pragma": "no-cache", - "replay-nonce": "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGH", - "server": "nginx", - "status": 200, - "strict-transport-security": "max-age=604800", - "url": "https://acme-v02.api.letsencrypt.org/acme/acct/46161", - "x-frame-options": "DENY" - } -output_text: - description: The raw text output - returned: always - type: str - sample: "{\\n \\\"id\\\": 12345,\\n \\\"key\\\": {\\n \\\"kty\\\": \\\"RSA\\\",\\n ..." -output_json: - description: The output parsed as JSON - returned: if output can be parsed as JSON - type: dict - sample: - - id: 12345 - - key: - - kty: RSA - - ... -''' - -from ansible.module_utils.acme import ( - ModuleFailException, - ACMEAccount, - handle_standard_module_arguments, - get_default_argspec, -) - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native, to_bytes - -import json - - -def main(): - argument_spec = get_default_argspec() - argument_spec.update(dict( - url=dict(type='str'), - method=dict(type='str', choices=['get', 'post', 'directory-only'], default='get'), - content=dict(type='str'), - fail_on_acme_error=dict(type='bool', default=True), - )) - module = AnsibleModule( - argument_spec=argument_spec, - mutually_exclusive=( - ['account_key_src', 'account_key_content'], - ), - required_if=( - ['method', 'get', ['url']], - ['method', 'post', ['url', 'content']], - ['method', 'get', ['account_key_src', 'account_key_content'], True], - ['method', 'post', ['account_key_src', 'account_key_content'], True], - ), - ) - handle_standard_module_arguments(module) - - result = dict() - changed = False - try: - # Get hold of ACMEAccount object (includes directory) - account = ACMEAccount(module) - method = module.params['method'] - result['directory'] = account.directory.directory - # Do we have to do more requests? - if method != 'directory-only': - url = module.params['url'] - fail_on_acme_error = module.params['fail_on_acme_error'] - # Do request - if method == 'get': - data, info = account.get_request(url, parse_json_result=False, fail_on_error=False) - elif method == 'post': - changed = True # only POSTs can change - data, info = account.send_signed_request(url, to_bytes(module.params['content']), parse_json_result=False, encode_payload=False) - # Update results - result.update(dict( - headers=info, - output_text=to_native(data), - )) - # See if we can parse the result as JSON - try: - result['output_json'] = json.loads(data) - except Exception as dummy: - pass - # Fail if error was returned - if fail_on_acme_error and info['status'] >= 400: - raise ModuleFailException("ACME request failed: CODE: {0} RESULT: {1}".format(info['status'], data)) - # Done! - module.exit_json(changed=changed, **result) - except ModuleFailException as e: - e.do_fail(module, **result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/certificate_complete_chain.py b/lib/ansible/modules/crypto/certificate_complete_chain.py deleted file mode 100644 index 77fbe2ee52..0000000000 --- a/lib/ansible/modules/crypto/certificate_complete_chain.py +++ /dev/null @@ -1,350 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -# -# (c) 2018, Felix Fontein <felix@fontein.de> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: certificate_complete_chain -author: "Felix Fontein (@felixfontein)" -version_added: "2.7" -short_description: Complete certificate chain given a set of untrusted and root certificates -description: - - "This module completes a given chain of certificates in PEM format by finding - intermediate certificates from a given set of certificates, until it finds a root - certificate in another given set of certificates." - - "This can for example be used to find the root certificate for a certificate chain - returned by M(acme_certificate)." - - "Note that this module does I(not) check for validity of the chains. It only - checks that issuer and subject match, and that the signature is correct. It - ignores validity dates and key usage completely. If you need to verify that a - generated chain is valid, please use C(openssl verify ...)." -requirements: - - "cryptography >= 1.5" -options: - input_chain: - description: - - A concatenated set of certificates in PEM format forming a chain. - - The module will try to complete this chain. - type: str - required: yes - root_certificates: - description: - - "A list of filenames or directories." - - "A filename is assumed to point to a file containing one or more certificates - in PEM format. All certificates in this file will be added to the set of - root certificates." - - "If a directory name is given, all files in the directory and its - subdirectories will be scanned and tried to be parsed as concatenated - certificates in PEM format." - - "Symbolic links will be followed." - type: list - elements: path - required: yes - intermediate_certificates: - description: - - "A list of filenames or directories." - - "A filename is assumed to point to a file containing one or more certificates - in PEM format. All certificates in this file will be added to the set of - root certificates." - - "If a directory name is given, all files in the directory and its - subdirectories will be scanned and tried to be parsed as concatenated - certificates in PEM format." - - "Symbolic links will be followed." - type: list - elements: path - default: [] -''' - - -EXAMPLES = ''' -# Given a leaf certificate for www.ansible.com and one or more intermediate -# certificates, finds the associated root certificate. -- name: Find root certificate - certificate_complete_chain: - input_chain: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com-fullchain.pem') }}" - root_certificates: - - /etc/ca-certificates/ - register: www_ansible_com -- name: Write root certificate to disk - copy: - dest: /etc/ssl/csr/www.ansible.com-root.pem - content: "{{ www_ansible_com.root }}" - -# Given a leaf certificate for www.ansible.com, and a list of intermediate -# certificates, finds the associated root certificate. -- name: Find root certificate - certificate_complete_chain: - input_chain: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.pem') }}" - intermediate_certificates: - - /etc/ssl/csr/www.ansible.com-chain.pem - root_certificates: - - /etc/ca-certificates/ - register: www_ansible_com -- name: Write complete chain to disk - copy: - dest: /etc/ssl/csr/www.ansible.com-completechain.pem - content: "{{ ''.join(www_ansible_com.complete_chain) }}" -- name: Write root chain (intermediates and root) to disk - copy: - dest: /etc/ssl/csr/www.ansible.com-rootchain.pem - content: "{{ ''.join(www_ansible_com.chain) }}" -''' - - -RETURN = ''' -root: - description: - - "The root certificate in PEM format." - returned: success - type: str -chain: - description: - - "The chain added to the given input chain. Includes the root certificate." - - "Returned as a list of PEM certificates." - returned: success - type: list - elements: str -complete_chain: - description: - - "The completed chain, including leaf, all intermediates, and root." - - "Returned as a list of PEM certificates." - returned: success - type: list - elements: str -''' - -import os -import traceback - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_bytes - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - import cryptography.hazmat.backends - import cryptography.hazmat.primitives.serialization - import cryptography.hazmat.primitives.asymmetric.rsa - import cryptography.hazmat.primitives.asymmetric.ec - import cryptography.hazmat.primitives.asymmetric.padding - import cryptography.hazmat.primitives.hashes - import cryptography.hazmat.primitives.asymmetric.utils - import cryptography.x509 - import cryptography.x509.oid - from distutils.version import LooseVersion - HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.5')) - _cryptography_backend = cryptography.hazmat.backends.default_backend() -except ImportError as dummy: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - HAS_CRYPTOGRAPHY = False - - -class Certificate(object): - ''' - Stores PEM with parsed certificate. - ''' - def __init__(self, pem, cert): - if not (pem.endswith('\n') or pem.endswith('\r')): - pem = pem + '\n' - self.pem = pem - self.cert = cert - - -def is_parent(module, cert, potential_parent): - ''' - Tests whether the given certificate has been issued by the potential parent certificate. - ''' - # Check issuer - if cert.cert.issuer != potential_parent.cert.subject: - return False - # Check signature - public_key = potential_parent.cert.public_key() - try: - if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey): - public_key.verify( - cert.cert.signature, - cert.cert.tbs_certificate_bytes, - cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15(), - cert.cert.signature_hash_algorithm - ) - elif isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey): - public_key.verify( - cert.cert.signature, - cert.cert.tbs_certificate_bytes, - cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cert.cert.signature_hash_algorithm), - ) - else: - # Unknown public key type - module.warn('Unknown public key type "{0}"'.format(public_key)) - return False - return True - except cryptography.exceptions.InvalidSignature as dummy: - return False - except Exception as e: - module.fail_json(msg='Unknown error on signature validation: {0}'.format(e)) - - -def parse_PEM_list(module, text, source, fail_on_error=True): - ''' - Parse concatenated PEM certificates. Return list of ``Certificate`` objects. - ''' - result = [] - lines = text.splitlines(True) - current = None - for line in lines: - if line.strip(): - if line.startswith('-----BEGIN '): - current = [line] - elif current is not None: - current.append(line) - if line.startswith('-----END '): - cert_pem = ''.join(current) - current = None - # Try to load PEM certificate - try: - cert = cryptography.x509.load_pem_x509_certificate(to_bytes(cert_pem), _cryptography_backend) - result.append(Certificate(cert_pem, cert)) - except Exception as e: - msg = 'Cannot parse certificate #{0} from {1}: {2}'.format(len(result) + 1, source, e) - if fail_on_error: - module.fail_json(msg=msg) - else: - module.warn(msg) - return result - - -def load_PEM_list(module, path, fail_on_error=True): - ''' - Load concatenated PEM certificates from file. Return list of ``Certificate`` objects. - ''' - try: - with open(path, "rb") as f: - return parse_PEM_list(module, f.read().decode('utf-8'), source=path, fail_on_error=fail_on_error) - except Exception as e: - msg = 'Cannot read certificate file {0}: {1}'.format(path, e) - if fail_on_error: - module.fail_json(msg=msg) - else: - module.warn(msg) - return [] - - -class CertificateSet(object): - ''' - Stores a set of certificates. Allows to search for parent (issuer of a certificate). - ''' - - def __init__(self, module): - self.module = module - self.certificates = set() - self.certificate_by_issuer = dict() - - def _load_file(self, path): - certs = load_PEM_list(self.module, path, fail_on_error=False) - for cert in certs: - self.certificates.add(cert) - self.certificate_by_issuer[cert.cert.subject] = cert - - def load(self, path): - ''' - Load lists of PEM certificates from a file or a directory. - ''' - b_path = to_bytes(path, errors='surrogate_or_strict') - if os.path.isdir(b_path): - for directory, dummy, files in os.walk(b_path, followlinks=True): - for file in files: - self._load_file(os.path.join(directory, file)) - else: - self._load_file(b_path) - - def find_parent(self, cert): - ''' - Search for the parent (issuer) of a certificate. Return ``None`` if none was found. - ''' - potential_parent = self.certificate_by_issuer.get(cert.cert.issuer) - if potential_parent is not None: - if is_parent(self.module, cert, potential_parent): - return potential_parent - return None - - -def format_cert(cert): - ''' - Return human readable representation of certificate for error messages. - ''' - return str(cert.cert) - - -def main(): - module = AnsibleModule( - argument_spec=dict( - input_chain=dict(type='str', required=True), - root_certificates=dict(type='list', required=True, elements='path'), - intermediate_certificates=dict(type='list', default=[], elements='path'), - ), - supports_check_mode=True, - ) - - if not HAS_CRYPTOGRAPHY: - module.fail_json(msg=missing_required_lib('cryptography >= 1.5'), exception=CRYPTOGRAPHY_IMP_ERR) - - # Load chain - chain = parse_PEM_list(module, module.params['input_chain'], source='input chain') - if len(chain) == 0: - module.fail_json(msg='Input chain must contain at least one certificate') - - # Check chain - for i, parent in enumerate(chain): - if i > 0: - if not is_parent(module, chain[i - 1], parent): - module.fail_json(msg=('Cannot verify input chain: certificate #{2}: {3} is not issuer ' + - 'of certificate #{0}: {1}').format(i, format_cert(chain[i - 1]), i + 1, format_cert(parent))) - - # Load intermediate certificates - intermediates = CertificateSet(module) - for path in module.params['intermediate_certificates']: - intermediates.load(path) - - # Load root certificates - roots = CertificateSet(module) - for path in module.params['root_certificates']: - roots.load(path) - - # Try to complete chain - current = chain[-1] - completed = [] - while current: - root = roots.find_parent(current) - if root: - completed.append(root) - break - intermediate = intermediates.find_parent(current) - if intermediate: - completed.append(intermediate) - current = intermediate - else: - module.fail_json(msg='Cannot complete chain. Stuck at certificate {0}'.format(format_cert(current))) - - # Return results - complete_chain = chain + completed - module.exit_json( - changed=False, - root=complete_chain[-1].pem, - chain=[cert.pem for cert in completed], - complete_chain=[cert.pem for cert in complete_chain], - ) - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/crypto/entrust/ecs_certificate.py b/lib/ansible/modules/crypto/entrust/ecs_certificate.py deleted file mode 100644 index dd1330653e..0000000000 --- a/lib/ansible/modules/crypto/entrust/ecs_certificate.py +++ /dev/null @@ -1,952 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright (c), Entrust Datacard Corporation, 2019 -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: ecs_certificate -author: - - Chris Trufan (@ctrufan) -version_added: '2.9' -short_description: Request SSL/TLS certificates with the Entrust Certificate Services (ECS) API -description: - - Create, reissue, and renew certificates with the Entrust Certificate Services (ECS) API. - - Requires credentials for the L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API. - - In order to request a certificate, the domain and organization used in the certificate signing request must be already - validated in the ECS system. It is I(not) the responsibility of this module to perform those steps. -notes: - - C(path) must be specified as the output location of the certificate. -requirements: - - cryptography >= 1.6 -options: - backup: - description: - - Whether a backup should be made for the certificate in I(path). - type: bool - default: false - force: - description: - - If force is used, a certificate is requested regardless of whether I(path) points to an existing valid certificate. - - If C(request_type=renew), a forced renew will fail if the certificate being renewed has been issued within the past 30 days, regardless of the - value of I(remaining_days) or the return value of I(cert_days) - the ECS API does not support the "renew" operation for certificates that are not - at least 30 days old. - type: bool - default: false - path: - description: - - The destination path for the generated certificate as a PEM encoded cert. - - If the certificate at this location is not an Entrust issued certificate, a new certificate will always be requested even if the current - certificate is technically valid. - - If there is already an Entrust certificate at this location, whether it is replaced is depends on the I(remaining_days) calculation. - - If an existing certificate is being replaced (see I(remaining_days), I(force), and I(tracking_id)), whether a new certificate is requested - or the existing certificate is renewed or reissued is based on I(request_type). - type: path - required: true - full_chain_path: - description: - - The destination path for the full certificate chain of the certificate, intermediates, and roots. - type: path - csr: - description: - - Base-64 encoded Certificate Signing Request (CSR). I(csr) is accepted with or without PEM formatting around the Base-64 string. - - If no I(csr) is provided when C(request_type=reissue) or C(request_type=renew), the certificate will be generated with the same public key as - the certificate being renewed or reissued. - - If I(subject_alt_name) is specified, it will override the subject alternate names in the CSR. - - If I(eku) is specified, it will override the extended key usage in the CSR. - - If I(ou) is specified, it will override the organizational units "ou=" present in the subject distinguished name of the CSR, if any. - - The organization "O=" field from the CSR will not be used. It will be replaced in the issued certificate by I(org) if present, and if not present, - the organization tied to I(client_id). - type: str - tracking_id: - description: - - The tracking ID of the certificate to reissue or renew. - - I(tracking_id) is invalid if C(request_type=new) or C(request_type=validate_only). - - If there is a certificate present in I(path) and it is an ECS certificate, I(tracking_id) will be ignored. - - If there is no certificate present in I(path) or there is but it is from another provider, the certificate represented by I(tracking_id) will - be renewed or reissued and saved to I(path). - - If there is no certificate present in I(path) and the I(force) and I(remaining_days) parameters do not indicate a new certificate is needed, - the certificate referenced by I(tracking_id) certificate will be saved to I(path). - - This can be used when a known certificate is not currently present on a server, but you want to renew or reissue it to be managed by an ansible - playbook. For example, if you specify C(request_type=renew), I(tracking_id) of an issued certificate, and I(path) to a file that does not exist, - the first run of a task will download the certificate specified by I(tracking_id) (assuming it is still valid). Future runs of the task will - (if applicable - see I(force) and I(remaining_days)) renew the certificate now present in I(path). - type: int - remaining_days: - description: - - The number of days the certificate must have left being valid. If C(cert_days < remaining_days) then a new certificate will be - obtained using I(request_type). - - If C(request_type=renew), a renewal will fail if the certificate being renewed has been issued within the past 30 days, so do not set a - I(remaining_days) value that is within 30 days of the full lifetime of the certificate being acted upon. (e.g. if you are requesting Certificates - with a 90 day lifetime, do not set remaining_days to a value C(60) or higher). - - The I(force) option may be used to ensure that a new certificate is always obtained. - type: int - default: 30 - request_type: - description: - - The operation performed if I(tracking_id) references a valid certificate to reissue, or there is already a certificate present in I(path) but - either I(force) is specified or C(cert_days < remaining_days). - - Specifying C(request_type=validate_only) means the request will be validated against the ECS API, but no certificate will be issued. - - Specifying C(request_type=new) means a certificate request will always be submitted and a new certificate issued. - - Specifying C(request_type=renew) means that an existing certificate (specified by I(tracking_id) if present, otherwise I(path)) will be renewed. - If there is no certificate to renew, a new certificate is requested. - - Specifying C(request_type=reissue) means that an existing certificate (specified by I(tracking_id) if present, otherwise I(path)) will be - reissued. - If there is no certificate to reissue, a new certificate is requested. - - If a certificate was issued within the past 30 days, the 'renew' operation is not a valid operation and will fail. - - Note that C(reissue) is an operation that will result in the revocation of the certificate that is reissued, be cautious with it's use. - - I(check_mode) is only supported if C(request_type=new) - - For example, setting C(request_type=renew) and C(remaining_days=30) and pointing to the same certificate on multiple playbook runs means that on - the first run new certificate will be requested. It will then be left along on future runs until it is within 30 days of expiry, then the - ECS "renew" operation will be performed. - type: str - choices: [ 'new', 'renew', 'reissue', 'validate_only'] - default: new - cert_type: - description: - - Specify the type of certificate requested. - - If a certificate is being reissued or renewed, this parameter is ignored, and the C(cert_type) of the initial certificate is used. - type: str - choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CODE_SIGNING', 'EV_CODE_SIGNING', - 'CDS_INDIVIDUAL', 'CDS_GROUP', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ] - subject_alt_name: - description: - - The subject alternative name identifiers, as an array of values (applies to I(cert_type) with a value of C(STANDARD_SSL), C(ADVANTAGE_SSL), - C(UC_SSL), C(EV_SSL), C(WILDCARD_SSL), C(PRIVATE_SSL), and C(PD_SSL)). - - If you are requesting a new SSL certificate, and you pass a I(subject_alt_name) parameter, any SAN names in the CSR are ignored. - If no subjectAltName parameter is passed, the SAN names in the CSR are used. - - See I(request_type) to understand more about SANs during reissues and renewals. - - In the case of certificates of type C(STANDARD_SSL) certificates, if the CN of the certificate is <domain>.<tld> only the www.<domain>.<tld> value - is accepted. If the CN of the certificate is www.<domain>.<tld> only the <domain>.<tld> value is accepted. - type: list - elements: str - eku: - description: - - If specified, overrides the key usage in the I(csr). - type: str - choices: [ SERVER_AUTH, CLIENT_AUTH, SERVER_AND_CLIENT_AUTH ] - ct_log: - description: - - In compliance with browser requirements, this certificate may be posted to the Certificate Transparency (CT) logs. This is a best practice - technique that helps domain owners monitor certificates issued to their domains. Note that not all certificates are eligible for CT logging. - - If I(ct_log) is not specified, the certificate uses the account default. - - If I(ct_log) is specified and the account settings allow it, I(ct_log) overrides the account default. - - If I(ct_log) is set to C(false), but the account settings are set to "always log", the certificate generation will fail. - type: bool - client_id: - description: - - The client ID to submit the Certificate Signing Request under. - - If no client ID is specified, the certificate will be submitted under the primary client with ID of 1. - - When using a client other than the primary client, the I(org) parameter cannot be specified. - - The issued certificate will have an organization value in the subject distinguished name represented by the client. - type: int - default: 1 - org: - description: - - Organization "O=" to include in the certificate. - - If I(org) is not specified, the organization from the client represented by I(client_id) is used. - - Unless the I(cert_type) is C(PD_SSL), this field may not be specified if the value of I(client_id) is not "1" (the primary client). - non-primary clients, certificates may only be issued with the organization of that client. - type: str - ou: - description: - - Organizational unit "OU=" to include in the certificate. - - I(ou) behavior is dependent on whether organizational units are enabled for your account. If organizational unit support is disabled for your - account, organizational units from the I(csr) and the I(ou) parameter are ignored. - - If both I(csr) and I(ou) are specified, the value in I(ou) will override the OU fields present in the subject distinguished name in the I(csr) - - If neither I(csr) nor I(ou) are specified for a renew or reissue operation, the OU fields in the initial certificate are reused. - - An invalid OU from I(csr) is ignored, but any invalid organizational units in I(ou) will result in an error indicating "Unapproved OU". The I(ou) - parameter can be used to force failure if an unapproved organizational unit is provided. - - A maximum of one OU may be specified for current products. Multiple OUs are reserved for future products. - type: list - elements: str - end_user_key_storage_agreement: - description: - - The end user of the Code Signing certificate must generate and store the private key for this request on cryptographically secure - hardware to be compliant with the Entrust CSP and Subscription agreement. If requesting a certificate of type C(CODE_SIGNING) or - C(EV_CODE_SIGNING), you must set I(end_user_key_storage_agreement) to true if and only if you acknowledge that you will inform the user of this - requirement. - - Applicable only to I(cert_type) of values C(CODE_SIGNING) and C(EV_CODE_SIGNING). - type: bool - tracking_info: - description: Free form tracking information to attach to the record for the certificate. - type: str - requester_name: - description: The requester name to associate with certificate tracking information. - type: str - required: true - requester_email: - description: The requester email to associate with certificate tracking information and receive delivery and expiry notices for the certificate. - type: str - required: true - requester_phone: - description: The requester phone number to associate with certificate tracking information. - type: str - required: true - additional_emails: - description: A list of additional email addresses to receive the delivery notice and expiry notification for the certificate. - type: list - elements: str - custom_fields: - description: - - Mapping of custom fields to associate with the certificate request and certificate. - - Only supported if custom fields are enabled for your account. - - Each custom field specified must be a custom field you have defined for your account. - type: dict - suboptions: - text1: - description: Custom text field (maximum 500 characters) - type: str - text2: - description: Custom text field (maximum 500 characters) - type: str - text3: - description: Custom text field (maximum 500 characters) - type: str - text4: - description: Custom text field (maximum 500 characters) - type: str - text5: - description: Custom text field (maximum 500 characters) - type: str - text6: - description: Custom text field (maximum 500 characters) - type: str - text7: - description: Custom text field (maximum 500 characters) - type: str - text8: - description: Custom text field (maximum 500 characters) - type: str - text9: - description: Custom text field (maximum 500 characters) - type: str - text10: - description: Custom text field (maximum 500 characters) - type: str - text11: - description: Custom text field (maximum 500 characters) - type: str - text12: - description: Custom text field (maximum 500 characters) - type: str - text13: - description: Custom text field (maximum 500 characters) - type: str - text14: - description: Custom text field (maximum 500 characters) - type: str - text15: - description: Custom text field (maximum 500 characters) - type: str - number1: - description: Custom number field. - type: float - number2: - description: Custom number field. - type: float - number3: - description: Custom number field. - type: float - number4: - description: Custom number field. - type: float - number5: - description: Custom number field. - type: float - date1: - description: Custom date field. - type: str - date2: - description: Custom date field. - type: str - date3: - description: Custom date field. - type: str - date4: - description: Custom date field. - type: str - date5: - description: Custom date field. - type: str - email1: - description: Custom email field. - type: str - email2: - description: Custom email field. - type: str - email3: - description: Custom email field. - type: str - email4: - description: Custom email field. - type: str - email5: - description: Custom email field. - type: str - dropdown1: - description: Custom dropdown field. - type: str - dropdown2: - description: Custom dropdown field. - type: str - dropdown3: - description: Custom dropdown field. - type: str - dropdown4: - description: Custom dropdown field. - type: str - dropdown5: - description: Custom dropdown field. - type: str - cert_expiry: - description: - - The date the certificate should be set to expire, in RFC3339 compliant date or date-time format. For example, - C(2020-02-23), C(2020-02-23T15:00:00.05Z). - - I(cert_expiry) is only supported for requests of C(request_type=new) or C(request_type=renew). If C(request_type=reissue), - I(cert_expiry) will be used for the first certificate issuance, but subsequent issuances will have the same expiry as the initial - certificate. - - A reissued certificate will always have the same expiry as the original certificate. - - Note that only the date (day, month, year) is supported for specifying the expiry date. If you choose to specify an expiry time with the expiry - date, the time will be adjusted to Eastern Standard Time (EST). This could have the unintended effect of moving your expiry date to the previous - day. - - Applies only to accounts with a pooling inventory model. - - Only one of I(cert_expiry) or I(cert_lifetime) may be specified. - type: str - cert_lifetime: - description: - - The lifetime of the certificate. - - Applies to all certificates for accounts with a non-pooling inventory model. - - I(cert_lifetime) is only supported for requests of C(request_type=new) or C(request_type=renew). If C(request_type=reissue), I(cert_lifetime) will - be used for the first certificate issuance, but subsequent issuances will have the same expiry as the initial certificate. - - Applies to certificates of I(cert_type)=C(CDS_INDIVIDUAL, CDS_GROUP, CDS_ENT_LITE, CDS_ENT_PRO, SMIME_ENT) for accounts with a pooling inventory - model. - - C(P1Y) is a certificate with a 1 year lifetime. - - C(P2Y) is a certificate with a 2 year lifetime. - - C(P3Y) is a certificate with a 3 year lifetime. - - Only one of I(cert_expiry) or I(cert_lifetime) may be specified. - type: str - choices: [ P1Y, P2Y, P3Y ] -seealso: - - module: openssl_privatekey - description: Can be used to create private keys (both for certificates and accounts). - - module: openssl_csr - description: Can be used to create a Certificate Signing Request (CSR). -extends_documentation_fragment: - - ecs_credential -''' - -EXAMPLES = r''' -- name: Request a new certificate from Entrust with bare minimum parameters. - Will request a new certificate if current one is valid but within 30 - days of expiry. If replacing an existing file in path, will back it up. - ecs_certificate: - backup: true - path: /etc/ssl/crt/ansible.com.crt - full_chain_path: /etc/ssl/crt/ansible.com.chain.crt - csr: /etc/ssl/csr/ansible.com.csr - cert_type: EV_SSL - requester_name: Jo Doe - requester_email: jdoe@ansible.com - requester_phone: 555-555-5555 - entrust_api_user: apiusername - entrust_api_key: a^lv*32!cd9LnT - entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt - entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key - -- name: If there is no certificate present in path, request a new certificate - of type EV_SSL. Otherwise, if there is an Entrust managed certificate - in path and it is within 63 days of expiration, request a renew of that - certificate. - ecs_certificate: - path: /etc/ssl/crt/ansible.com.crt - csr: /etc/ssl/csr/ansible.com.csr - cert_type: EV_SSL - cert_expiry: '2020-08-20' - request_type: renew - remaining_days: 63 - requester_name: Jo Doe - requester_email: jdoe@ansible.com - requester_phone: 555-555-5555 - entrust_api_user: apiusername - entrust_api_key: a^lv*32!cd9LnT - entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt - entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key - -- name: If there is no certificate present in path, download certificate - specified by tracking_id if it is still valid. Otherwise, if the - certificate is within 79 days of expiration, request a renew of that - certificate and save it in path. This can be used to "migrate" a - certificate to be Ansible managed. - ecs_certificate: - path: /etc/ssl/crt/ansible.com.crt - csr: /etc/ssl/csr/ansible.com.csr - tracking_id: 2378915 - request_type: renew - remaining_days: 79 - entrust_api_user: apiusername - entrust_api_key: a^lv*32!cd9LnT - entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt - entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key - -- name: Force a reissue of the certificate specified by tracking_id. - ecs_certificate: - path: /etc/ssl/crt/ansible.com.crt - force: true - tracking_id: 2378915 - request_type: reissue - entrust_api_user: apiusername - entrust_api_key: a^lv*32!cd9LnT - entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt - entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key - -- name: Request a new certificate with an alternative client. Note that the - issued certificate will have it's Subject Distinguished Name use the - organization details associated with that client, rather than what is - in the CSR. - ecs_certificate: - path: /etc/ssl/crt/ansible.com.crt - csr: /etc/ssl/csr/ansible.com.csr - client_id: 2 - requester_name: Jo Doe - requester_email: jdoe@ansible.com - requester_phone: 555-555-5555 - entrust_api_user: apiusername - entrust_api_key: a^lv*32!cd9LnT - entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt - entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key - -- name: Request a new certificate with a number of CSR parameters overridden - and tracking information - ecs_certificate: - path: /etc/ssl/crt/ansible.com.crt - full_chain_path: /etc/ssl/crt/ansible.com.chain.crt - csr: /etc/ssl/csr/ansible.com.csr - subject_alt_name: - - ansible.testcertificates.com - - www.testcertificates.com - eku: SERVER_AND_CLIENT_AUTH - ct_log: true - org: Test Organization Inc. - ou: - - Administration - tracking_info: "Submitted via Ansible" - additional_emails: - - itsupport@testcertificates.com - - jsmith@ansible.com - custom_fields: - text1: Admin - text2: Invoice 25 - number1: 342 - date1: '2018-01-01' - email1: sales@ansible.testcertificates.com - dropdown1: red - cert_expiry: '2020-08-15' - requester_name: Jo Doe - requester_email: jdoe@ansible.com - requester_phone: 555-555-5555 - entrust_api_user: apiusername - entrust_api_key: a^lv*32!cd9LnT - entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt - entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key - -''' - -RETURN = ''' -filename: - description: The destination path for the generated certificate. - returned: changed or success - type: str - sample: /etc/ssl/crt/www.ansible.com.crt -backup_file: - description: Name of backup file created for the certificate. - returned: changed and if I(backup) is C(true) - type: str - sample: /path/to/www.ansible.com.crt.2019-03-09@11:22~ -backup_full_chain_file: - description: Name of the backup file created for the certificate chain. - returned: changed and if I(backup) is C(true) and I(full_chain_path) is set. - type: str - sample: /path/to/ca.chain.crt.2019-03-09@11:22~ -tracking_id: - description: The tracking ID to reference and track the certificate in ECS. - returned: success - type: int - sample: 380079 -serial_number: - description: The serial number of the issued certificate. - returned: success - type: int - sample: 1235262234164342 -cert_days: - description: The number of days the certificate remains valid. - returned: success - type: int - sample: 253 -cert_status: - description: - - The certificate status in ECS. - - 'Current possible values (which may be expanded in the future) are: C(ACTIVE), C(APPROVED), C(DEACTIVATED), C(DECLINED), C(EXPIRED), C(NA), - C(PENDING), C(PENDING_QUORUM), C(READY), C(REISSUED), C(REISSUING), C(RENEWED), C(RENEWING), C(REVOKED), C(SUSPENDED)' - returned: success - type: str - sample: ACTIVE -cert_details: - description: - - The full response JSON from the Get Certificate call of the ECS API. - - 'While the response contents are guaranteed to be forwards compatible with new ECS API releases, Entrust recommends that you do not make any - playbooks take actions based on the content of this field. However it may be useful for debugging, logging, or auditing purposes.' - returned: success - type: dict - -''' - -from ansible.module_utils.ecs.api import ( - ecs_client_argument_spec, - ECSClient, - RestOperationException, - SessionConfigurationException, -) - -import datetime -import json -import os -import re -import time -import traceback -from distutils.version import LooseVersion - -from ansible.module_utils import crypto as crypto_utils -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native, to_bytes - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - -MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' - - -def validate_cert_expiry(cert_expiry): - search_string_partial = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])\Z') - search_string_full = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):' - r'([0-5][0-9]|60)(.[0-9]+)?(([Zz])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))\Z') - if search_string_partial.match(cert_expiry) or search_string_full.match(cert_expiry): - return True - return False - - -def calculate_cert_days(expires_after): - cert_days = 0 - if expires_after: - expires_after_datetime = datetime.datetime.strptime(expires_after, '%Y-%m-%dT%H:%M:%SZ') - cert_days = (expires_after_datetime - datetime.datetime.now()).days - return cert_days - - -# Populate the value of body[dict_param_name] with the JSON equivalent of -# module parameter of param_name if that parameter is present, otherwise leave field -# out of resulting dict -def convert_module_param_to_json_bool(module, dict_param_name, param_name): - body = {} - if module.params[param_name] is not None: - if module.params[param_name]: - body[dict_param_name] = 'true' - else: - body[dict_param_name] = 'false' - return body - - -class EcsCertificate(object): - ''' - Entrust Certificate Services certificate class. - ''' - - def __init__(self, module): - self.path = module.params['path'] - self.full_chain_path = module.params['full_chain_path'] - self.force = module.params['force'] - self.backup = module.params['backup'] - self.request_type = module.params['request_type'] - self.csr = module.params['csr'] - - # All return values - self.changed = False - self.filename = None - self.tracking_id = None - self.cert_status = None - self.serial_number = None - self.cert_days = None - self.cert_details = None - self.backup_file = None - self.backup_full_chain_file = None - - self.cert = None - self.ecs_client = None - if self.path and os.path.exists(self.path): - try: - self.cert = crypto_utils.load_certificate(self.path, backend='cryptography') - except Exception as dummy: - self.cert = None - # Instantiate the ECS client and then try a no-op connection to verify credentials are valid - try: - self.ecs_client = ECSClient( - entrust_api_user=module.params['entrust_api_user'], - entrust_api_key=module.params['entrust_api_key'], - entrust_api_cert=module.params['entrust_api_client_cert_path'], - entrust_api_cert_key=module.params['entrust_api_client_cert_key_path'], - entrust_api_specification_path=module.params['entrust_api_specification_path'] - ) - except SessionConfigurationException as e: - module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e))) - try: - self.ecs_client.GetAppVersion() - except RestOperationException as e: - module.fail_json(msg='Please verify credential information. Received exception when testing ECS connection: {0}'.format(to_native(e.message))) - - # Conversion of the fields that go into the 'tracking' parameter of the request object - def convert_tracking_params(self, module): - body = {} - tracking = {} - if module.params['requester_name']: - tracking['requesterName'] = module.params['requester_name'] - if module.params['requester_email']: - tracking['requesterEmail'] = module.params['requester_email'] - if module.params['requester_phone']: - tracking['requesterPhone'] = module.params['requester_phone'] - if module.params['tracking_info']: - tracking['trackingInfo'] = module.params['tracking_info'] - if module.params['custom_fields']: - # Omit custom fields from submitted dict if not present, instead of submitting them with value of 'null' - # The ECS API does technically accept null without error, but it complicates debugging user escalations and is unnecessary bandwidth. - custom_fields = {} - for k, v in module.params['custom_fields'].items(): - if v is not None: - custom_fields[k] = v - tracking['customFields'] = custom_fields - if module.params['additional_emails']: - tracking['additionalEmails'] = module.params['additional_emails'] - body['tracking'] = tracking - return body - - def convert_cert_subject_params(self, module): - body = {} - if module.params['subject_alt_name']: - body['subjectAltName'] = module.params['subject_alt_name'] - if module.params['org']: - body['org'] = module.params['org'] - if module.params['ou']: - body['ou'] = module.params['ou'] - return body - - def convert_general_params(self, module): - body = {} - if module.params['eku']: - body['eku'] = module.params['eku'] - if self.request_type == 'new': - body['certType'] = module.params['cert_type'] - body['clientId'] = module.params['client_id'] - body.update(convert_module_param_to_json_bool(module, 'ctLog', 'ct_log')) - body.update(convert_module_param_to_json_bool(module, 'endUserKeyStorageAgreement', 'end_user_key_storage_agreement')) - return body - - def convert_expiry_params(self, module): - body = {} - if module.params['cert_lifetime']: - body['certLifetime'] = module.params['cert_lifetime'] - elif module.params['cert_expiry']: - body['certExpiryDate'] = module.params['cert_expiry'] - # If neither cerTLifetime or certExpiryDate was specified and the request type is new, default to 365 days - elif self.request_type != 'reissue': - gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())) - expiry = gmt_now + datetime.timedelta(days=365) - body['certExpiryDate'] = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z") - return body - - def set_tracking_id_by_serial_number(self, module): - try: - # Use serial_number to identify if certificate is an Entrust Certificate - # with an associated tracking ID - serial_number = "{0:X}".format(self.cert.serial_number) - cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {}) - if len(cert_results) == 1: - self.tracking_id = cert_results[0].get('trackingId') - except RestOperationException as dummy: - # If we fail to find a cert by serial number, that's fine, we just don't set self.tracking_id - return - - def set_cert_details(self, module): - try: - self.cert_details = self.ecs_client.GetCertificate(trackingId=self.tracking_id) - self.cert_status = self.cert_details.get('status') - self.serial_number = self.cert_details.get('serialNumber') - self.cert_days = calculate_cert_days(self.cert_details.get('expiresAfter')) - except RestOperationException as e: - module.fail_json('Failed to get details of certificate with tracking_id="{0}", Error: '.format(self.tracking_id), to_native(e.message)) - - def check(self, module): - if self.cert: - # We will only treat a certificate as valid if it is found as a managed entrust cert. - # We will only set updated tracking ID based on certificate in "path" if it is managed by entrust. - self.set_tracking_id_by_serial_number(module) - - if module.params['tracking_id'] and self.tracking_id and module.params['tracking_id'] != self.tracking_id: - module.warn('tracking_id parameter of "{0}" provided, but will be ignored. Valid certificate was present in path "{1}" with ' - 'tracking_id of "{2}".'.format(module.params['tracking_id'], self.path, self.tracking_id)) - - # If we did not end up setting tracking_id based on existing cert, get from module params - if not self.tracking_id: - self.tracking_id = module.params['tracking_id'] - - if not self.tracking_id: - return False - - self.set_cert_details(module) - - if self.cert_status == 'EXPIRED' or self.cert_status == 'SUSPENDED' or self.cert_status == 'REVOKED': - return False - if self.cert_days < module.params['remaining_days']: - return False - - return True - - def request_cert(self, module): - if not self.check(module) or self.force: - body = {} - - # Read the CSR contents - if self.csr and os.path.exists(self.csr): - with open(self.csr, 'r') as csr_file: - body['csr'] = csr_file.read() - - # Check if the path is already a cert - # tracking_id may be set as a parameter or by get_cert_details if an entrust cert is in 'path'. If tracking ID is null - # We will be performing a reissue operation. - if self.request_type != 'new' and not self.tracking_id: - module.warn('No existing Entrust certificate found in path={0} and no tracking_id was provided, setting request_type to "new" for this task' - 'run. Future playbook runs that point to the pathination file in {1} will use request_type={2}' - .format(self.path, self.path, self.request_type)) - self.request_type = 'new' - elif self.request_type == 'new' and self.tracking_id: - module.warn('Existing certificate being acted upon, but request_type is "new", so will be a new certificate issuance rather than a' - 'reissue or renew') - # Use cases where request type is new and no existing certificate, or where request type is reissue/renew and a valid - # existing certificate is found, do not need warnings. - - body.update(self.convert_tracking_params(module)) - body.update(self.convert_cert_subject_params(module)) - body.update(self.convert_general_params(module)) - body.update(self.convert_expiry_params(module)) - - if not module.check_mode: - try: - if self.request_type == 'validate_only': - body['validateOnly'] = 'true' - result = self.ecs_client.NewCertRequest(Body=body) - if self.request_type == 'new': - result = self.ecs_client.NewCertRequest(Body=body) - elif self.request_type == 'renew': - result = self.ecs_client.RenewCertRequest(trackingId=self.tracking_id, Body=body) - elif self.request_type == 'reissue': - result = self.ecs_client.ReissueCertRequest(trackingId=self.tracking_id, Body=body) - self.tracking_id = result.get('trackingId') - self.set_cert_details(module) - except RestOperationException as e: - module.fail_json(msg='Failed to request new certificate from Entrust (ECS) {0}'.format(e.message)) - - if self.request_type != 'validate_only': - if self.backup: - self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, to_bytes(self.cert_details.get('endEntityCert'))) - if self.full_chain_path and self.cert_details.get('chainCerts'): - if self.backup: - self.backup_full_chain_file = module.backup_local(self.full_chain_path) - chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n' - crypto_utils.write_file(module, to_bytes(chain_string), path=self.full_chain_path) - self.changed = True - # If there is no certificate present in path but a tracking ID was specified, save it to disk - elif not os.path.exists(self.path) and self.tracking_id: - if not module.check_mode: - crypto_utils.write_file(module, to_bytes(self.cert_details.get('endEntityCert'))) - if self.full_chain_path and self.cert_details.get('chainCerts'): - chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n' - crypto_utils.write_file(module, to_bytes(chain_string), path=self.full_chain_path) - self.changed = True - - def dump(self): - result = { - 'changed': self.changed, - 'filename': self.path, - 'tracking_id': self.tracking_id, - 'cert_status': self.cert_status, - 'serial_number': self.serial_number, - 'cert_days': self.cert_days, - 'cert_details': self.cert_details, - } - if self.backup_file: - result['backup_file'] = self.backup_file - result['backup_full_chain_file'] = self.backup_full_chain_file - return result - - -def custom_fields_spec(): - return dict( - text1=dict(type='str'), - text2=dict(type='str'), - text3=dict(type='str'), - text4=dict(type='str'), - text5=dict(type='str'), - text6=dict(type='str'), - text7=dict(type='str'), - text8=dict(type='str'), - text9=dict(type='str'), - text10=dict(type='str'), - text11=dict(type='str'), - text12=dict(type='str'), - text13=dict(type='str'), - text14=dict(type='str'), - text15=dict(type='str'), - number1=dict(type='float'), - number2=dict(type='float'), - number3=dict(type='float'), - number4=dict(type='float'), - number5=dict(type='float'), - date1=dict(type='str'), - date2=dict(type='str'), - date3=dict(type='str'), - date4=dict(type='str'), - date5=dict(type='str'), - email1=dict(type='str'), - email2=dict(type='str'), - email3=dict(type='str'), - email4=dict(type='str'), - email5=dict(type='str'), - dropdown1=dict(type='str'), - dropdown2=dict(type='str'), - dropdown3=dict(type='str'), - dropdown4=dict(type='str'), - dropdown5=dict(type='str'), - ) - - -def ecs_certificate_argument_spec(): - return dict( - backup=dict(type='bool', default=False), - force=dict(type='bool', default=False), - path=dict(type='path', required=True), - full_chain_path=dict(type='path'), - tracking_id=dict(type='int'), - remaining_days=dict(type='int', default=30), - request_type=dict(type='str', default='new', choices=['new', 'renew', 'reissue', 'validate_only']), - cert_type=dict(type='str', choices=['STANDARD_SSL', - 'ADVANTAGE_SSL', - 'UC_SSL', - 'EV_SSL', - 'WILDCARD_SSL', - 'PRIVATE_SSL', - 'PD_SSL', - 'CODE_SIGNING', - 'EV_CODE_SIGNING', - 'CDS_INDIVIDUAL', - 'CDS_GROUP', - 'CDS_ENT_LITE', - 'CDS_ENT_PRO', - 'SMIME_ENT', - ]), - csr=dict(type='str'), - subject_alt_name=dict(type='list', elements='str'), - eku=dict(type='str', choices=['SERVER_AUTH', 'CLIENT_AUTH', 'SERVER_AND_CLIENT_AUTH']), - ct_log=dict(type='bool'), - client_id=dict(type='int', default=1), - org=dict(type='str'), - ou=dict(type='list', elements='str'), - end_user_key_storage_agreement=dict(type='bool'), - tracking_info=dict(type='str'), - requester_name=dict(type='str', required=True), - requester_email=dict(type='str', required=True), - requester_phone=dict(type='str', required=True), - additional_emails=dict(type='list', elements='str'), - custom_fields=dict(type='dict', default=None, options=custom_fields_spec()), - cert_expiry=dict(type='str'), - cert_lifetime=dict(type='str', choices=['P1Y', 'P2Y', 'P3Y']), - ) - - -def main(): - ecs_argument_spec = ecs_client_argument_spec() - ecs_argument_spec.update(ecs_certificate_argument_spec()) - module = AnsibleModule( - argument_spec=ecs_argument_spec, - required_if=( - ['request_type', 'new', ['cert_type']], - ['request_type', 'validate_only', ['cert_type']], - ['cert_type', 'CODE_SIGNING', ['end_user_key_storage_agreement']], - ['cert_type', 'EV_CODE_SIGNING', ['end_user_key_storage_agreement']], - ), - mutually_exclusive=( - ['cert_expiry', 'cert_lifetime'], - ), - supports_check_mode=True, - ) - - if not CRYPTOGRAPHY_FOUND or CRYPTOGRAPHY_VERSION < LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION): - module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), - exception=CRYPTOGRAPHY_IMP_ERR) - - # If validate_only is used, pointing to an existing tracking_id is an invalid operation - if module.params['tracking_id']: - if module.params['request_type'] == 'new' or module.params['request_type'] == 'validate_only': - module.fail_json(msg='The tracking_id field is invalid when request_type="{0}".'.format(module.params['request_type'])) - - # A reissued request can not specify an expiration date or lifetime - if module.params['request_type'] == 'reissue': - if module.params['cert_expiry']: - module.fail_json(msg='The cert_expiry field is invalid when request_type="reissue".') - elif module.params['cert_lifetime']: - module.fail_json(msg='The cert_lifetime field is invalid when request_type="reissue".') - # Only a reissued request can omit the CSR - else: - module_params_csr = module.params['csr'] - if module_params_csr is None: - module.fail_json(msg='The csr field is required when request_type={0}'.format(module.params['request_type'])) - elif not os.path.exists(module_params_csr): - module.fail_json(msg='The csr field of {0} was not a valid path. csr is required when request_type={1}'.format( - module_params_csr, module.params['request_type'])) - - if module.params['ou'] and len(module.params['ou']) > 1: - module.fail_json(msg='Multiple "ou" values are not currently supported.') - - if module.params['end_user_key_storage_agreement']: - if module.params['cert_type'] != 'CODE_SIGNING' and module.params['cert_type'] != 'EV_CODE_SIGNING': - module.fail_json(msg='Parameter "end_user_key_storage_agreement" is valid only for cert_types "CODE_SIGNING" and "EV_CODE_SIGNING"') - - if module.params['org'] and module.params['client_id'] != 1 and module.params['cert_type'] != 'PD_SSL': - module.fail_json(msg='The "org" parameter is not supported when client_id parameter is set to a value other than 1, unless cert_type is "PD_SSL".') - - if module.params['cert_expiry']: - if not validate_cert_expiry(module.params['cert_expiry']): - module.fail_json(msg='The "cert_expiry" parameter of "{0}" is not a valid date or date-time'.format(module.params['cert_expiry'])) - - certificate = EcsCertificate(module) - certificate.request_cert(module) - result = certificate.dump() - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/entrust/ecs_domain.py b/lib/ansible/modules/crypto/entrust/ecs_domain.py deleted file mode 100644 index cda4dea53a..0000000000 --- a/lib/ansible/modules/crypto/entrust/ecs_domain.py +++ /dev/null @@ -1,409 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright 2019 Entrust Datacard Corporation. -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - - -DOCUMENTATION = ''' ---- -module: ecs_domain -author: - - Chris Trufan (@ctrufan) -version_added: '2.10' -short_description: Request validation of a domain with the Entrust Certificate Services (ECS) API -description: - - Request validation or re-validation of a domain with the Entrust Certificate Services (ECS) API. - - Requires credentials for the L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API. - - If the domain is already in the validation process, no new validation will be requested, but the validation data (if applicable) will be returned. - - If the domain is already in the validation process but the I(verification_method) specified is different than the current I(verification_method), - the I(verification_method) will be updated and validation data (if applicable) will be returned. - - If the domain is an active, validated domain, the return value of I(changed) will be false, unless C(domain_status=EXPIRED), in which case a re-validation - will be performed. - - If C(verification_method=dns), details about the required DNS entry will be specified in the return parameters I(dns_contents), I(dns_location), and - I(dns_resource_type). - - If C(verification_method=web_server), details about the required file details will be specified in the return parameters I(file_contents) and - I(file_location). - - If C(verification_method=email), the email address(es) that the validation email(s) were sent to will be in the return parameter I(emails). This is - purely informational. For domains requested using this module, this will always be a list of size 1. -notes: - - There is a small delay (typically about 5 seconds, but can be as long as 60 seconds) before obtaining the random values when requesting a validation - while C(verification_method=dns) or C(verification_method=web_server). Be aware of that if doing many domain validation requests. -options: - client_id: - description: - - The client ID to request the domain be associated with. - - If no client ID is specified, the domain will be added under the primary client with ID of 1. - type: int - default: 1 - domain_name: - description: - - The domain name to be verified or reverified. - type: str - required: true - verification_method: - description: - - The verification method to be used to prove control of the domain. - - If C(verification_method=email) and the value I(verification_email) is specified, that value is used for the email validation. If - I(verification_email) is not provided, the first value present in WHOIS data will be used. An email will be sent to the address in - I(verification_email) with instructions on how to verify control of the domain. - - If C(verification_method=dns), the value I(dns_contents) must be stored in location I(dns_location), with a DNS record type of - I(verification_dns_record_type). To prove domain ownership, update your DNS records so the text string returned by I(dns_contents) is available at - I(dns_location). - - If C(verification_method=web_server), the contents of return value I(file_contents) must be made available on a web server accessible at location - I(file_location). - - If C(verification_method=manual), the domain will be validated with a manual process. This is not recommended. - type: str - choices: [ 'dns', 'email', 'manual', 'web_server'] - required: true - verification_email: - description: - - Email address to be used to verify domain ownership. - - 'Email address must be either an email address present in the WHOIS data for I(domain_name), or one of the following constructed emails: - admin@I(domain_name), administrator@I(domain_name), webmaster@I(domain_name), hostmaster@I(domain_name), postmaster@I(domain_name)' - - 'Note that if I(domain_name) includes subdomains, the top level domain should be used. For example, if requesting validation of - example1.ansible.com, or test.example2.ansible.com, and you want to use the "admin" preconstructed name, the email address should be - admin@ansible.com.' - - If using the email values from the WHOIS data for the domain or it's top level namespace, they must be exact matches. - - If C(verification_method=email) but I(verification_email) is not provided, the first email address found in WHOIS data for the domain will be - used. - - To verify domain ownership, domain owner must follow the instructions in the email they receive. - - Only allowed if C(verification_method=email) - type: str -seealso: - - module: openssl_certificate - description: Can be used to request certificates from ECS, with C(provider=entrust). - - module: ecs_certificate - description: Can be used to request a Certificate from ECS using a verified domain. -extends_documentation_fragment: - - ecs_credential -''' - -EXAMPLES = r''' -- name: Request domain validation using email validation for client ID of 2. - ecs_domain: - domain_name: ansible.com - client_id: 2 - verification_method: email - verification_email: admin@ansible.com - entrust_api_user: apiusername - entrust_api_key: a^lv*32!cd9LnT - entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt - entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key - -- name: Request domain validation using DNS. If domain is already valid, - request revalidation if expires within 90 days - ecs_domain: - domain_name: ansible.com - verification_method: dns - entrust_api_user: apiusername - entrust_api_key: a^lv*32!cd9LnT - entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt - entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key - -- name: Request domain validation using web server validation, and revalidate - if fewer than 60 days remaining of EV eligibility. - ecs_domain: - domain_name: ansible.com - verification_method: web_server - entrust_api_user: apiusername - entrust_api_key: a^lv*32!cd9LnT - entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt - entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key - -- name: Request domain validation using manual validation. - ecs_domain: - domain_name: ansible.com - verification_method: manual - entrust_api_user: apiusername - entrust_api_key: a^lv*32!cd9LnT - entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt - entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key -''' - -RETURN = ''' -domain_status: - description: Status of the current domain. Will be one of C(APPROVED), C(DECLINED), C(CANCELLED), C(INITIAL_VERIFICATION), C(DECLINED), C(CANCELLED), - C(RE_VERIFICATION), C(EXPIRED), C(EXPIRING) - returned: changed or success - type: str - sample: APPROVED -verification_method: - description: Verification method used to request the domain validation. If C(changed) will be the same as I(verification_method) input parameter. - returned: changed or success - type: str - sample: dns -file_location: - description: The location that ECS will be expecting to be able to find the file for domain verification, containing the contents of I(file_contents). - returned: I(verification_method) is C(web_server) - type: str - sample: http://ansible.com/.well-known/pki-validation/abcd.txt -file_contents: - description: The contents of the file that ECS will be expecting to find at C(file_location). - returned: I(verification_method) is C(web_server) - type: str - sample: AB23CD41432522FF2526920393982FAB -emails: - description: - - The list of emails used to request validation of this domain. - - Domains requested using this module will only have a list of size 1. - returned: I(verification_method) is C(email) - type: list - sample: [ admin@ansible.com, administrator@ansible.com ] -dns_location: - description: The location that ECS will be expecting to be able to find the DNS entry for domain verification, containing the contents of I(dns_contents). - returned: changed and if I(verification_method) is C(dns) - type: str - sample: _pki-validation.ansible.com -dns_contents: - description: The value that ECS will be expecting to find in the DNS record located at I(dns_location). - returned: changed and if I(verification_method) is C(dns) - type: str - sample: AB23CD41432522FF2526920393982FAB -dns_resource_type: - description: The type of resource record that ECS will be expecting for the DNS record located at I(dns_location). - returned: changed and if I(verification_method) is C(dns) - type: str - sample: TXT -client_id: - description: Client ID that the domain belongs to. If the input value I(client_id) is specified, this will always be the same as I(client_id) - returned: changed or success - type: int - sample: 1 -ov_eligible: - description: Whether the domain is eligible for submission of "OV" certificates. Will never be C(false) if I(ov_eligible) is C(true) - returned: success and I(domain_status) is C(APPROVED), C(RE_VERIFICATION), C(EXPIRING), or C(EXPIRED). - type: bool - sample: true -ov_days_remaining: - description: The number of days the domain remains eligible for submission of "OV" certificates. Will never be less than the value of I(ev_days_remaining) - returned: success and I(ov_eligible) is C(true) and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING). - type: int - sample: 129 -ev_eligible: - description: Whether the domain is eligible for submission of "EV" certificates. Will never be C(true) if I(ov_eligible) is C(false) - returned: success and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING), or C(EXPIRED). - type: bool - sample: true -ev_days_remaining: - description: The number of days the domain remains eligible for submission of "EV" certificates. Will never be greater than the value of - I(ov_days_remaining) - returned: success and I(ev_eligible) is C(true) and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING). - type: int - sample: 94 - -''' - -from ansible.module_utils.ecs.api import ( - ecs_client_argument_spec, - ECSClient, - RestOperationException, - SessionConfigurationException, -) - -import datetime -import time -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native - - -def calculate_days_remaining(expiry_date): - days_remaining = None - if expiry_date: - expiry_datetime = datetime.datetime.strptime(expiry_date, '%Y-%m-%dT%H:%M:%SZ') - days_remaining = (expiry_datetime - datetime.datetime.now()).days - return days_remaining - - -class EcsDomain(object): - ''' - Entrust Certificate Services domain class. - ''' - - def __init__(self, module): - self.changed = False - self.domain_status = None - self.verification_method = None - self.file_location = None - self.file_contents = None - self.dns_location = None - self.dns_contents = None - self.dns_resource_type = None - self.emails = None - self.ov_eligible = None - self.ov_days_remaining = None - self.ev_eligble = None - self.ev_days_remaining = None - # Note that verification_method is the 'current' verification - # method of the domain, we'll use module.params when requesting a new - # one, in case the verification method has changed. - self.verification_method = None - - self.ecs_client = None - # Instantiate the ECS client and then try a no-op connection to verify credentials are valid - try: - self.ecs_client = ECSClient( - entrust_api_user=module.params['entrust_api_user'], - entrust_api_key=module.params['entrust_api_key'], - entrust_api_cert=module.params['entrust_api_client_cert_path'], - entrust_api_cert_key=module.params['entrust_api_client_cert_key_path'], - entrust_api_specification_path=module.params['entrust_api_specification_path'] - ) - except SessionConfigurationException as e: - module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e))) - try: - self.ecs_client.GetAppVersion() - except RestOperationException as e: - module.fail_json(msg='Please verify credential information. Received exception when testing ECS connection: {0}'.format(to_native(e.message))) - - def set_domain_details(self, domain_details): - if domain_details.get('verificationMethod'): - self.verification_method = domain_details['verificationMethod'].lower() - self.domain_status = domain_details['verificationStatus'] - self.ov_eligible = domain_details.get('ovEligible') - self.ov_days_remaining = calculate_days_remaining(domain_details.get('ovExpiry')) - self.ev_eligible = domain_details.get('evEligible') - self.ev_days_remaining = calculate_days_remaining(domain_details.get('evExpiry')) - self.client_id = domain_details['clientId'] - - if self.verification_method == 'dns' and domain_details.get('dnsMethod'): - self.dns_location = domain_details['dnsMethod']['recordDomain'] - self.dns_resource_type = domain_details['dnsMethod']['recordType'] - self.dns_contents = domain_details['dnsMethod']['recordValue'] - elif self.verification_method == 'web_server' and domain_details.get('webServerMethod'): - self.file_location = domain_details['webServerMethod']['fileLocation'] - self.file_contents = domain_details['webServerMethod']['fileContents'] - elif self.verification_method == 'email' and domain_details.get('emailMethod'): - self.emails = domain_details['emailMethod'] - - def check(self, module): - try: - domain_details = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name']) - self.set_domain_details(domain_details) - if self.domain_status != 'APPROVED' and self.domain_status != 'INITIAL_VERIFICATION' and self.domain_status != 'RE_VERIFICATION': - return False - - # If domain verification is in process, we want to return the random values and treat it as a valid. - if self.domain_status == 'INITIAL_VERIFICATION' or self.domain_status == 'RE_VERIFICATION': - # Unless the verification method has changed, in which case we need to do a reverify request. - if self.verification_method != module.params['verification_method']: - return False - - if self.domain_status == 'EXPIRING': - return False - - return True - except RestOperationException as dummy: - return False - - def request_domain(self, module): - if not self.check(module): - body = {} - - body['verificationMethod'] = module.params['verification_method'].upper() - if module.params['verification_method'] == 'email': - emailMethod = {} - if module.params['verification_email']: - emailMethod['emailSource'] = 'SPECIFIED' - emailMethod['email'] = module.params['verification_email'] - else: - emailMethod['emailSource'] = 'INCLUDE_WHOIS' - body['emailMethod'] = emailMethod - # Only populate domain name in body if it is not an existing domain - if not self.domain_status: - body['domainName'] = module.params['domain_name'] - try: - if not self.domain_status: - self.ecs_client.AddDomain(clientId=module.params['client_id'], Body=body) - else: - self.ecs_client.ReverifyDomain(clientId=module.params['client_id'], domain=module.params['domain_name'], Body=body) - - time.sleep(5) - result = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name']) - - # It takes a bit of time before the random values are available - if module.params['verification_method'] == 'dns' or module.params['verification_method'] == 'web_server': - for i in range(4): - # Check both that random values are now available, and that they're different than were populated by previous 'check' - if module.params['verification_method'] == 'dns': - if result.get('dnsMethod') and result['dnsMethod']['recordValue'] != self.dns_contents: - break - elif module.params['verification_method'] == 'web_server': - if result.get('webServerMethod') and result['webServerMethod']['fileContents'] != self.file_contents: - break - time.sleep(10) - result = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name']) - self.changed = True - self.set_domain_details(result) - except RestOperationException as e: - module.fail_json(msg='Failed to request domain validation from Entrust (ECS) {0}'.format(e.message)) - - def dump(self): - result = { - 'changed': self.changed, - 'client_id': self.client_id, - 'domain_status': self.domain_status, - } - - if self.verification_method: - result['verification_method'] = self.verification_method - if self.ov_eligible is not None: - result['ov_eligible'] = self.ov_eligible - if self.ov_days_remaining: - result['ov_days_remaining'] = self.ov_days_remaining - if self.ev_eligible is not None: - result['ev_eligible'] = self.ev_eligible - if self.ev_days_remaining: - result['ev_days_remaining'] = self.ev_days_remaining - if self.emails: - result['emails'] = self.emails - - if self.verification_method == 'dns': - result['dns_location'] = self.dns_location - result['dns_contents'] = self.dns_contents - result['dns_resource_type'] = self.dns_resource_type - elif self.verification_method == 'web_server': - result['file_location'] = self.file_location - result['file_contents'] = self.file_contents - elif self.verification_method == 'email': - result['emails'] = self.emails - - return result - - -def ecs_domain_argument_spec(): - return dict( - client_id=dict(type='int', default=1), - domain_name=dict(type='str', required=True), - verification_method=dict(type='str', required=True, choices=['dns', 'email', 'manual', 'web_server']), - verification_email=dict(type='str'), - ) - - -def main(): - ecs_argument_spec = ecs_client_argument_spec() - ecs_argument_spec.update(ecs_domain_argument_spec()) - module = AnsibleModule( - argument_spec=ecs_argument_spec, - supports_check_mode=False, - ) - - if module.params['verification_email'] and module.params['verification_method'] != 'email': - module.fail_json(msg='The verification_email field is invalid when verification_method="{0}".'.format(module.params['verification_method'])) - - domain = EcsDomain(module) - domain.request_domain(module) - result = domain.dump() - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/get_certificate.py b/lib/ansible/modules/crypto/get_certificate.py deleted file mode 100644 index 29b5c7ff01..0000000000 --- a/lib/ansible/modules/crypto/get_certificate.py +++ /dev/null @@ -1,371 +0,0 @@ -#!/usr/bin/python -# coding: utf-8 -*- - -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: get_certificate -author: "John Westcott IV (@john-westcott-iv)" -version_added: "2.8" -short_description: Get a certificate from a host:port -description: - - Makes a secure connection and returns information about the presented certificate - - The module can use the cryptography Python library, or the pyOpenSSL Python - library. By default, it tries to detect which one is available. This can be - overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL - backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13." -options: - host: - description: - - The host to get the cert for (IP is fine) - type: str - required: true - ca_cert: - description: - - A PEM file containing one or more root certificates; if present, the cert will be validated against these root certs. - - Note that this only validates the certificate is signed by the chain; not that the cert is valid for the host presenting it. - type: path - port: - description: - - The port to connect to - type: int - required: true - proxy_host: - description: - - Proxy host used when get a certificate. - type: str - version_added: 2.9 - proxy_port: - description: - - Proxy port used when get a certificate. - type: int - default: 8080 - version_added: 2.9 - timeout: - description: - - The timeout in seconds - type: int - default: 10 - 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(pyopenssl). - - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. - - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. - type: str - default: auto - choices: [ auto, cryptography, pyopenssl ] - version_added: "2.9" - -notes: - - When using ca_cert on OS X it has been reported that in some conditions the validate will always succeed. - -requirements: - - "python >= 2.7 when using C(proxy_host)" - - "cryptography >= 1.6 or pyOpenSSL >= 0.15" -''' - -RETURN = ''' -cert: - description: The certificate retrieved from the port - returned: success - type: str -expired: - description: Boolean indicating if the cert is expired - returned: success - type: bool -extensions: - description: Extensions applied to the cert - returned: success - type: list - elements: dict - contains: - critical: - returned: success - type: bool - description: Whether the extension is critical. - asn1_data: - returned: success - type: str - description: The Base64 encoded ASN.1 content of the extnesion. - name: - returned: success - type: str - description: The extension's name. -issuer: - description: Information about the issuer of the cert - returned: success - type: dict -not_after: - description: Expiration date of the cert - returned: success - type: str -not_before: - description: Issue date of the cert - returned: success - type: str -serial_number: - description: The serial number of the cert - returned: success - type: str -signature_algorithm: - description: The algorithm used to sign the cert - returned: success - type: str -subject: - description: Information about the subject of the cert (OU, CN, etc) - returned: success - type: dict -version: - description: The version number of the certificate - returned: success - type: str -''' - -EXAMPLES = ''' -- name: Get the cert from an RDP port - get_certificate: - host: "1.2.3.4" - port: 3389 - delegate_to: localhost - run_once: true - register: cert - -- name: Get a cert from an https port - get_certificate: - host: "www.google.com" - port: 443 - delegate_to: localhost - run_once: true - register: cert - -- name: How many days until cert expires - debug: - msg: "cert expires in: {{ expire_days }} days." - vars: - expire_days: "{{ (( cert.not_after | to_datetime('%Y%m%d%H%M%SZ')) - (ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ')) ).days }}" -''' - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_bytes -from ansible.module_utils import crypto as crypto_utils - -from distutils.version import LooseVersion -from os.path import isfile -from socket import setdefaulttimeout, socket -from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_OPTIONAL - -import atexit -import base64 -import datetime -import traceback - -MINIMAL_PYOPENSSL_VERSION = '0.15' -MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' - -CREATE_DEFAULT_CONTEXT_IMP_ERR = None -try: - from ssl import create_default_context -except ImportError: - CREATE_DEFAULT_CONTEXT_IMP_ERR = traceback.format_exc() - HAS_CREATE_DEFAULT_CONTEXT = False -else: - HAS_CREATE_DEFAULT_CONTEXT = True - -PYOPENSSL_IMP_ERR = None -try: - import OpenSSL - from OpenSSL import crypto - PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) -except ImportError: - PYOPENSSL_IMP_ERR = traceback.format_exc() - PYOPENSSL_FOUND = False -else: - PYOPENSSL_FOUND = True - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - import cryptography.exceptions - import cryptography.x509 - from cryptography.hazmat.backends import default_backend as cryptography_backend - CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - - -def main(): - module = AnsibleModule( - argument_spec=dict( - ca_cert=dict(type='path'), - host=dict(type='str', required=True), - port=dict(type='int', required=True), - proxy_host=dict(type='str'), - proxy_port=dict(type='int', default=8080), - timeout=dict(type='int', default=10), - select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'), - ), - ) - - ca_cert = module.params.get('ca_cert') - host = module.params.get('host') - port = module.params.get('port') - proxy_host = module.params.get('proxy_host') - proxy_port = module.params.get('proxy_port') - timeout = module.params.get('timeout') - - backend = module.params.get('select_crypto_backend') - if backend == 'auto': - # Detection what is possible - can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) - can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) - - # First try cryptography, then pyOpenSSL - if can_use_cryptography: - backend = 'cryptography' - elif can_use_pyopenssl: - backend = 'pyopenssl' - - # Success? - if backend == 'auto': - module.fail_json(msg=("Can't detect any of the required Python libraries " - "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( - MINIMAL_CRYPTOGRAPHY_VERSION, - MINIMAL_PYOPENSSL_VERSION)) - - if backend == 'pyopenssl': - if not PYOPENSSL_FOUND: - module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), - exception=PYOPENSSL_IMP_ERR) - module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') - elif backend == 'cryptography': - if not CRYPTOGRAPHY_FOUND: - module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), - exception=CRYPTOGRAPHY_IMP_ERR) - - result = dict( - changed=False, - ) - - if timeout: - setdefaulttimeout(timeout) - - if ca_cert: - if not isfile(ca_cert): - module.fail_json(msg="ca_cert file does not exist") - - if proxy_host: - if not HAS_CREATE_DEFAULT_CONTEXT: - module.fail_json(msg='To use proxy_host, you must run the get_certificate module with Python 2.7 or newer.', - exception=CREATE_DEFAULT_CONTEXT_IMP_ERR) - - try: - connect = "CONNECT %s:%s HTTP/1.0\r\n\r\n" % (host, port) - sock = socket() - atexit.register(sock.close) - sock.connect((proxy_host, proxy_port)) - sock.send(connect.encode()) - sock.recv(8192) - - ctx = create_default_context() - ctx.check_hostname = False - ctx.verify_mode = CERT_NONE - - if ca_cert: - ctx.verify_mode = CERT_OPTIONAL - ctx.load_verify_locations(cafile=ca_cert) - - cert = ctx.wrap_socket(sock, server_hostname=host).getpeercert(True) - cert = DER_cert_to_PEM_cert(cert) - except Exception as e: - module.fail_json(msg="Failed to get cert from port with error: {0}".format(e)) - - else: - try: - cert = get_server_certificate((host, port), ca_certs=ca_cert) - except Exception as e: - module.fail_json(msg="Failed to get cert from port with error: {0}".format(e)) - - result['cert'] = cert - - if backend == 'pyopenssl': - x509 = crypto.load_certificate(crypto.FILETYPE_PEM, cert) - result['subject'] = {} - for component in x509.get_subject().get_components(): - result['subject'][component[0]] = component[1] - - result['expired'] = x509.has_expired() - - result['extensions'] = [] - extension_count = x509.get_extension_count() - for index in range(0, extension_count): - extension = x509.get_extension(index) - result['extensions'].append({ - 'critical': extension.get_critical(), - 'asn1_data': extension.get_data(), - 'name': extension.get_short_name(), - }) - - result['issuer'] = {} - for component in x509.get_issuer().get_components(): - result['issuer'][component[0]] = component[1] - - result['not_after'] = x509.get_notAfter() - result['not_before'] = x509.get_notBefore() - - result['serial_number'] = x509.get_serial_number() - result['signature_algorithm'] = x509.get_signature_algorithm() - - result['version'] = x509.get_version() - - elif backend == 'cryptography': - x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography_backend()) - result['subject'] = {} - for attribute in x509.subject: - result['subject'][crypto_utils.cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value - - result['expired'] = x509.not_valid_after < datetime.datetime.utcnow() - - result['extensions'] = [] - for dotted_number, entry in crypto_utils.cryptography_get_extensions_from_cert(x509).items(): - oid = cryptography.x509.oid.ObjectIdentifier(dotted_number) - result['extensions'].append({ - 'critical': entry['critical'], - 'asn1_data': base64.b64decode(entry['value']), - 'name': crypto_utils.cryptography_oid_to_name(oid, short=True), - }) - - result['issuer'] = {} - for attribute in x509.issuer: - result['issuer'][crypto_utils.cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value - - result['not_after'] = x509.not_valid_after.strftime('%Y%m%d%H%M%SZ') - result['not_before'] = x509.not_valid_before.strftime('%Y%m%d%H%M%SZ') - - result['serial_number'] = x509.serial_number - result['signature_algorithm'] = crypto_utils.cryptography_oid_to_name(x509.signature_algorithm_oid) - - # We need the -1 offset to get the same values as pyOpenSSL - if x509.version == cryptography.x509.Version.v1: - result['version'] = 1 - 1 - elif x509.version == cryptography.x509.Version.v3: - result['version'] = 3 - 1 - else: - result['version'] = "unknown" - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/luks_device.py b/lib/ansible/modules/crypto/luks_device.py deleted file mode 100644 index 3b895db757..0000000000 --- a/lib/ansible/modules/crypto/luks_device.py +++ /dev/null @@ -1,794 +0,0 @@ -#!/usr/bin/python -# Copyright (c) 2017 Ansible Project -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import (absolute_import, division, print_function) -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = ''' ---- -module: luks_device - -short_description: Manage encrypted (LUKS) devices - -version_added: "2.8" - -description: - - "Module manages L(LUKS,https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup) - on given device. Supports creating, destroying, opening and closing of - LUKS container and adding or removing new keys and passphrases." - -options: - device: - description: - - "Device to work with (e.g. C(/dev/sda1)). Needed in most cases. - Can be omitted only when I(state=closed) together with I(name) - is provided." - type: str - state: - description: - - "Desired state of the LUKS container. Based on its value creates, - destroys, opens or closes the LUKS container on a given device." - - "I(present) will create LUKS container unless already present. - Requires I(device) and either I(keyfile) or I(passphrase) options - to be provided." - - "I(absent) will remove existing LUKS container if it exists. - Requires I(device) or I(name) to be specified." - - "I(opened) will unlock the LUKS container. If it does not exist - it will be created first. - Requires I(device) and either I(keyfile) or I(passphrase) - to be specified. Use the I(name) option to set the name of - the opened container. Otherwise the name will be - generated automatically and returned as a part of the - result." - - "I(closed) will lock the LUKS container. However if the container - does not exist it will be created. - Requires I(device) and either I(keyfile) or I(passphrase) - options to be provided. If container does already exist - I(device) or I(name) will suffice." - type: str - default: present - choices: [present, absent, opened, closed] - name: - description: - - "Sets container name when I(state=opened). Can be used - instead of I(device) when closing the existing container - (i.e. when I(state=closed))." - type: str - keyfile: - description: - - "Used to unlock the container. Either a I(keyfile) or a - I(passphrase) is needed for most of the operations. Parameter - value is the path to the keyfile with the passphrase." - - "BEWARE that working with keyfiles in plaintext is dangerous. - Make sure that they are protected." - type: path - passphrase: - description: - - "Used to unlock the container. Either a I(passphrase) or a - I(keyfile) is needed for most of the operations. Parameter - value is a string with the passphrase." - type: str - version_added: '2.10' - keysize: - description: - - "Sets the key size only if LUKS container does not exist." - type: int - version_added: '2.10' - new_keyfile: - description: - - "Adds additional key to given container on I(device). - Needs I(keyfile) or I(passphrase) option for authorization. - LUKS container supports up to 8 keyslots. Parameter value - is the path to the keyfile with the passphrase." - - "NOTE that adding additional keys is *not idempotent*. - A new keyslot will be used even if another keyslot already - exists for this keyfile." - - "BEWARE that working with keyfiles in plaintext is dangerous. - Make sure that they are protected." - type: path - new_passphrase: - description: - - "Adds additional passphrase to given container on I(device). - Needs I(keyfile) or I(passphrase) option for authorization. LUKS - container supports up to 8 keyslots. Parameter value is a string - with the new passphrase." - - "NOTE that adding additional passphrase is *not idempotent*. A - new keyslot will be used even if another keyslot already exists - for this passphrase." - type: str - version_added: '2.10' - remove_keyfile: - description: - - "Removes given key from the container on I(device). Does not - remove the keyfile from filesystem. - Parameter value is the path to the keyfile with the passphrase." - - "NOTE that removing keys is *not idempotent*. Trying to remove - a key which no longer exists results in an error." - - "NOTE that to remove the last key from a LUKS container, the - I(force_remove_last_key) option must be set to C(yes)." - - "BEWARE that working with keyfiles in plaintext is dangerous. - Make sure that they are protected." - type: path - remove_passphrase: - description: - - "Removes given passphrase from the container on I(device). - Parameter value is a string with the passphrase to remove." - - "NOTE that removing passphrases is I(not - idempotent). Trying to remove a passphrase which no longer - exists results in an error." - - "NOTE that to remove the last keyslot from a LUKS - container, the I(force_remove_last_key) option must be set - to C(yes)." - type: str - version_added: '2.10' - force_remove_last_key: - description: - - "If set to C(yes), allows removing the last key from a container." - - "BEWARE that when the last key has been removed from a container, - the container can no longer be opened!" - type: bool - default: no - label: - description: - - "This option allow the user to create a LUKS2 format container - with label support, respectively to identify the container by - label on later usages." - - "Will only be used on container creation, or when I(device) is - not specified." - - "This cannot be specified if I(type) is set to C(luks1)." - type: str - version_added: "2.10" - uuid: - description: - - "With this option user can identify the LUKS container by UUID." - - "Will only be used when I(device) and I(label) are not specified." - type: str - version_added: "2.10" - type: - description: - - "This option allow the user explicit define the format of LUKS - container that wants to work with. Options are C(luks1) or C(luks2)" - type: str - choices: [luks1, luks2] - version_added: "2.10" - - - -requirements: - - "cryptsetup" - - "wipefs (when I(state) is C(absent))" - - "lsblk" - - "blkid (when I(label) or I(uuid) options are used)" - -author: Jan Pokorny (@japokorn) -''' - -EXAMPLES = ''' - -- name: create LUKS container (remains unchanged if it already exists) - luks_device: - device: "/dev/loop0" - state: "present" - keyfile: "/vault/keyfile" - -- name: create LUKS container with a passphrase - luks_device: - device: "/dev/loop0" - state: "present" - passphrase: "foo" - -- name: (create and) open the LUKS container; name it "mycrypt" - luks_device: - device: "/dev/loop0" - state: "opened" - name: "mycrypt" - keyfile: "/vault/keyfile" - -- name: close the existing LUKS container "mycrypt" - luks_device: - state: "closed" - name: "mycrypt" - -- name: make sure LUKS container exists and is closed - luks_device: - device: "/dev/loop0" - state: "closed" - keyfile: "/vault/keyfile" - -- name: create container if it does not exist and add new key to it - luks_device: - device: "/dev/loop0" - state: "present" - keyfile: "/vault/keyfile" - new_keyfile: "/vault/keyfile2" - -- name: add new key to the LUKS container (container has to exist) - luks_device: - device: "/dev/loop0" - keyfile: "/vault/keyfile" - new_keyfile: "/vault/keyfile2" - -- name: add new passphrase to the LUKS container - luks_device: - device: "/dev/loop0" - keyfile: "/vault/keyfile" - new_passphrase: "foo" - -- name: remove existing keyfile from the LUKS container - luks_device: - device: "/dev/loop0" - remove_keyfile: "/vault/keyfile2" - -- name: remove existing passphrase from the LUKS container - luks_device: - device: "/dev/loop0" - remove_passphrase: "foo" - -- name: completely remove the LUKS container and its contents - luks_device: - device: "/dev/loop0" - state: "absent" - -- name: create a container with label - luks_device: - device: "/dev/loop0" - state: "present" - keyfile: "/vault/keyfile" - label: personalLabelName - -- name: open the LUKS container based on label without device; name it "mycrypt" - luks_device: - label: "personalLabelName" - state: "opened" - name: "mycrypt" - keyfile: "/vault/keyfile" - -- name: close container based on UUID - luks_device: - uuid: 03ecd578-fad4-4e6c-9348-842e3e8fa340 - state: "closed" - name: "mycrypt" - -- name: create a container using luks2 format - luks_device: - device: "/dev/loop0" - state: "present" - keyfile: "/vault/keyfile" - type: luks2 -''' - -RETURN = ''' -name: - description: - When I(state=opened) returns (generated or given) name - of LUKS container. Returns None if no name is supplied. - returned: success - type: str - sample: "luks-c1da9a58-2fde-4256-9d9f-6ab008b4dd1b" -''' - -import os -import re -import stat - -from ansible.module_utils.basic import AnsibleModule - -RETURN_CODE = 0 -STDOUT = 1 -STDERR = 2 - -# used to get <luks-name> out of lsblk output in format 'crypt <luks-name>' -# regex takes care of any possible blank characters -LUKS_NAME_REGEX = re.compile(r'\s*crypt\s+([^\s]*)\s*') -# used to get </luks/device> out of lsblk output -# in format 'device: </luks/device>' -LUKS_DEVICE_REGEX = re.compile(r'\s*device:\s+([^\s]*)\s*') - - -class Handler(object): - - def __init__(self, module): - self._module = module - self._lsblk_bin = self._module.get_bin_path('lsblk', True) - - def _run_command(self, command, data=None): - return self._module.run_command(command, data=data) - - def get_device_by_uuid(self, uuid): - ''' Returns the device that holds UUID passed by user - ''' - self._blkid_bin = self._module.get_bin_path('blkid', True) - uuid = self._module.params['uuid'] - if uuid is None: - return None - result = self._run_command([self._blkid_bin, '--uuid', uuid]) - if result[RETURN_CODE] != 0: - return None - return result[STDOUT].strip() - - def get_device_by_label(self, label): - ''' Returns the device that holds label passed by user - ''' - self._blkid_bin = self._module.get_bin_path('blkid', True) - label = self._module.params['label'] - if label is None: - return None - result = self._run_command([self._blkid_bin, '--label', label]) - if result[RETURN_CODE] != 0: - return None - return result[STDOUT].strip() - - def generate_luks_name(self, device): - ''' Generate name for luks based on device UUID ('luks-<UUID>'). - Raises ValueError when obtaining of UUID fails. - ''' - result = self._run_command([self._lsblk_bin, '-n', device, '-o', 'UUID']) - - if result[RETURN_CODE] != 0: - raise ValueError('Error while generating LUKS name for %s: %s' - % (device, result[STDERR])) - dev_uuid = result[STDOUT].strip() - return 'luks-%s' % dev_uuid - - -class CryptHandler(Handler): - - def __init__(self, module): - super(CryptHandler, self).__init__(module) - self._cryptsetup_bin = self._module.get_bin_path('cryptsetup', True) - - def get_container_name_by_device(self, device): - ''' obtain LUKS container name based on the device where it is located - return None if not found - raise ValueError if lsblk command fails - ''' - result = self._run_command([self._lsblk_bin, device, '-nlo', 'type,name']) - if result[RETURN_CODE] != 0: - raise ValueError('Error while obtaining LUKS name for %s: %s' - % (device, result[STDERR])) - - m = LUKS_NAME_REGEX.search(result[STDOUT]) - - try: - name = m.group(1) - except AttributeError: - name = None - return name - - def get_container_device_by_name(self, name): - ''' obtain device name based on the LUKS container name - return None if not found - raise ValueError if lsblk command fails - ''' - # apparently each device can have only one LUKS container on it - result = self._run_command([self._cryptsetup_bin, 'status', name]) - if result[RETURN_CODE] != 0: - return None - - m = LUKS_DEVICE_REGEX.search(result[STDOUT]) - device = m.group(1) - return device - - def is_luks(self, device): - ''' check if the LUKS container does exist - ''' - result = self._run_command([self._cryptsetup_bin, 'isLuks', device]) - return result[RETURN_CODE] == 0 - - def run_luks_create(self, device, keyfile, passphrase, keysize): - # create a new luks container; use batch mode to auto confirm - luks_type = self._module.params['type'] - label = self._module.params['label'] - - options = [] - if keysize is not None: - options.append('--key-size=' + str(keysize)) - if label is not None: - options.extend(['--label', label]) - luks_type = 'luks2' - if luks_type is not None: - options.extend(['--type', luks_type]) - - args = [self._cryptsetup_bin, 'luksFormat'] - args.extend(options) - args.extend(['-q', device]) - if keyfile: - args.append(keyfile) - - result = self._run_command(args, data=passphrase) - if result[RETURN_CODE] != 0: - raise ValueError('Error while creating LUKS on %s: %s' - % (device, result[STDERR])) - - def run_luks_open(self, device, keyfile, passphrase, name): - args = [self._cryptsetup_bin] - if keyfile: - args.extend(['--key-file', keyfile]) - args.extend(['open', '--type', 'luks', device, name]) - - result = self._run_command(args, data=passphrase) - if result[RETURN_CODE] != 0: - raise ValueError('Error while opening LUKS container on %s: %s' - % (device, result[STDERR])) - - def run_luks_close(self, name): - result = self._run_command([self._cryptsetup_bin, 'close', name]) - if result[RETURN_CODE] != 0: - raise ValueError('Error while closing LUKS container %s' % (name)) - - def run_luks_remove(self, device): - wipefs_bin = self._module.get_bin_path('wipefs', True) - - name = self.get_container_name_by_device(device) - if name is not None: - self.run_luks_close(name) - result = self._run_command([wipefs_bin, '--all', device]) - if result[RETURN_CODE] != 0: - raise ValueError('Error while wiping luks container %s: %s' - % (device, result[STDERR])) - - def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile, - new_passphrase): - ''' Add new key from a keyfile or passphrase to given 'device'; - authentication done using 'keyfile' or 'passphrase'. - Raises ValueError when command fails. - ''' - data = [] - args = [self._cryptsetup_bin, 'luksAddKey', device] - - if keyfile: - args.extend(['--key-file', keyfile]) - else: - data.append(passphrase) - - if new_keyfile: - args.append(new_keyfile) - else: - data.extend([new_passphrase, new_passphrase]) - - result = self._run_command(args, data='\n'.join(data) or None) - if result[RETURN_CODE] != 0: - raise ValueError('Error while adding new LUKS keyslot to %s: %s' - % (device, result[STDERR])) - - def run_luks_remove_key(self, device, keyfile, passphrase, - force_remove_last_key=False): - ''' Remove key from given device - Raises ValueError when command fails - ''' - if not force_remove_last_key: - result = self._run_command([self._cryptsetup_bin, 'luksDump', device]) - if result[RETURN_CODE] != 0: - raise ValueError('Error while dumping LUKS header from %s' - % (device, )) - keyslot_count = 0 - keyslot_area = False - keyslot_re = re.compile(r'^Key Slot [0-9]+: ENABLED') - for line in result[STDOUT].splitlines(): - if line.startswith('Keyslots:'): - keyslot_area = True - elif line.startswith(' '): - # LUKS2 header dumps use human-readable indented output. - # Thus we have to look out for 'Keyslots:' and count the - # number of indented keyslot numbers. - if keyslot_area and line[2] in '0123456789': - keyslot_count += 1 - elif line.startswith('\t'): - pass - elif keyslot_re.match(line): - # LUKS1 header dumps have one line per keyslot with ENABLED - # or DISABLED in them. We count such lines with ENABLED. - keyslot_count += 1 - else: - keyslot_area = False - if keyslot_count < 2: - self._module.fail_json(msg="LUKS device %s has less than two active keyslots. " - "To be able to remove a key, please set " - "`force_remove_last_key` to `yes`." % device) - - args = [self._cryptsetup_bin, 'luksRemoveKey', device, '-q'] - if keyfile: - args.extend(['--key-file', keyfile]) - result = self._run_command(args, data=passphrase) - if result[RETURN_CODE] != 0: - raise ValueError('Error while removing LUKS key from %s: %s' - % (device, result[STDERR])) - - -class ConditionsHandler(Handler): - - def __init__(self, module, crypthandler): - super(ConditionsHandler, self).__init__(module) - self._crypthandler = crypthandler - self.device = self.get_device_name() - - def get_device_name(self): - device = self._module.params.get('device') - label = self._module.params.get('label') - uuid = self._module.params.get('uuid') - name = self._module.params.get('name') - - if device is None and label is not None: - device = self.get_device_by_label(label) - elif device is None and uuid is not None: - device = self.get_device_by_uuid(uuid) - elif device is None and name is not None: - device = self._crypthandler.get_container_device_by_name(name) - - return device - - def luks_create(self): - return (self.device is not None and - (self._module.params['keyfile'] is not None or - self._module.params['passphrase'] is not None) and - self._module.params['state'] in ('present', - 'opened', - 'closed') and - not self._crypthandler.is_luks(self.device)) - - def opened_luks_name(self): - ''' If luks is already opened, return its name. - If 'name' parameter is specified and differs - from obtained value, fail. - Return None otherwise - ''' - if self._module.params['state'] != 'opened': - return None - - # try to obtain luks name - it may be already opened - name = self._crypthandler.get_container_name_by_device(self.device) - - if name is None: - # container is not open - return None - - if self._module.params['name'] is None: - # container is already opened - return name - - if name != self._module.params['name']: - # the container is already open but with different name: - # suspicious. back off - self._module.fail_json(msg="LUKS container is already opened " - "under different name '%s'." % name) - - # container is opened and the names match - return name - - def luks_open(self): - if ((self._module.params['keyfile'] is None and - self._module.params['passphrase'] is None) or - self.device is None or - self._module.params['state'] != 'opened'): - # conditions for open not fulfilled - return False - - name = self.opened_luks_name() - - if name is None: - return True - return False - - def luks_close(self): - if ((self._module.params['name'] is None and self.device is None) or - self._module.params['state'] != 'closed'): - # conditions for close not fulfilled - return False - - if self.device is not None: - name = self._crypthandler.get_container_name_by_device(self.device) - # successfully getting name based on device means that luks is open - luks_is_open = name is not None - - if self._module.params['name'] is not None: - self.device = self._crypthandler.get_container_device_by_name( - self._module.params['name']) - # successfully getting device based on name means that luks is open - luks_is_open = self.device is not None - - return luks_is_open - - def luks_add_key(self): - if (self.device is None or - (self._module.params['keyfile'] is None and - self._module.params['passphrase'] is None) or - (self._module.params['new_keyfile'] is None and - self._module.params['new_passphrase'] is None)): - # conditions for adding a key not fulfilled - return False - - if self._module.params['state'] == 'absent': - self._module.fail_json(msg="Contradiction in setup: Asking to " - "add a key to absent LUKS.") - - return True - - def luks_remove_key(self): - if (self.device is None or - (self._module.params['remove_keyfile'] is None and - self._module.params['remove_passphrase'] is None)): - # conditions for removing a key not fulfilled - return False - - if self._module.params['state'] == 'absent': - self._module.fail_json(msg="Contradiction in setup: Asking to " - "remove a key from absent LUKS.") - - return True - - def luks_remove(self): - return (self.device is not None and - self._module.params['state'] == 'absent' and - self._crypthandler.is_luks(self.device)) - - -def run_module(): - # available arguments/parameters that a user can pass - module_args = dict( - state=dict(type='str', default='present', choices=['present', 'absent', 'opened', 'closed']), - device=dict(type='str'), - name=dict(type='str'), - keyfile=dict(type='path'), - new_keyfile=dict(type='path'), - remove_keyfile=dict(type='path'), - passphrase=dict(type='str', no_log=True), - new_passphrase=dict(type='str', no_log=True), - remove_passphrase=dict(type='str', no_log=True), - force_remove_last_key=dict(type='bool', default=False), - keysize=dict(type='int'), - label=dict(type='str'), - uuid=dict(type='str'), - type=dict(type='str', choices=['luks1', 'luks2']), - ) - - mutually_exclusive = [ - ('keyfile', 'passphrase'), - ('new_keyfile', 'new_passphrase'), - ('remove_keyfile', 'remove_passphrase') - ] - - # seed the result dict in the object - result = dict( - changed=False, - name=None - ) - - module = AnsibleModule(argument_spec=module_args, - supports_check_mode=True, - mutually_exclusive=mutually_exclusive) - - if module.params['device'] is not None: - try: - statinfo = os.stat(module.params['device']) - mode = statinfo.st_mode - if not stat.S_ISBLK(mode) and not stat.S_ISCHR(mode): - raise Exception('{0} is not a device'.format(module.params['device'])) - except Exception as e: - module.fail_json(msg=str(e)) - - crypt = CryptHandler(module) - conditions = ConditionsHandler(module, crypt) - - # conditions not allowed to run - if module.params['label'] is not None and module.params['type'] == 'luks1': - module.fail_json(msg='You cannot combine type luks1 with the label option.') - - # The conditions are in order to allow more operations in one run. - # (e.g. create luks and add a key to it) - - # luks create - if conditions.luks_create(): - if not module.check_mode: - try: - crypt.run_luks_create(conditions.device, - module.params['keyfile'], - module.params['passphrase'], - module.params['keysize']) - except ValueError as e: - module.fail_json(msg="luks_device error: %s" % e) - result['changed'] = True - if module.check_mode: - module.exit_json(**result) - - # luks open - - name = conditions.opened_luks_name() - if name is not None: - result['name'] = name - - if conditions.luks_open(): - name = module.params['name'] - if name is None: - try: - name = crypt.generate_luks_name(conditions.device) - except ValueError as e: - module.fail_json(msg="luks_device error: %s" % e) - if not module.check_mode: - try: - crypt.run_luks_open(conditions.device, - module.params['keyfile'], - module.params['passphrase'], - name) - except ValueError as e: - module.fail_json(msg="luks_device error: %s" % e) - result['name'] = name - result['changed'] = True - if module.check_mode: - module.exit_json(**result) - - # luks close - if conditions.luks_close(): - if conditions.device is not None: - try: - name = crypt.get_container_name_by_device( - conditions.device) - except ValueError as e: - module.fail_json(msg="luks_device error: %s" % e) - else: - name = module.params['name'] - if not module.check_mode: - try: - crypt.run_luks_close(name) - except ValueError as e: - module.fail_json(msg="luks_device error: %s" % e) - result['name'] = name - result['changed'] = True - if module.check_mode: - module.exit_json(**result) - - # luks add key - if conditions.luks_add_key(): - if not module.check_mode: - try: - crypt.run_luks_add_key(conditions.device, - module.params['keyfile'], - module.params['passphrase'], - module.params['new_keyfile'], - module.params['new_passphrase']) - except ValueError as e: - module.fail_json(msg="luks_device error: %s" % e) - result['changed'] = True - if module.check_mode: - module.exit_json(**result) - - # luks remove key - if conditions.luks_remove_key(): - if not module.check_mode: - try: - last_key = module.params['force_remove_last_key'] - crypt.run_luks_remove_key(conditions.device, - module.params['remove_keyfile'], - module.params['remove_passphrase'], - force_remove_last_key=last_key) - except ValueError as e: - module.fail_json(msg="luks_device error: %s" % e) - result['changed'] = True - if module.check_mode: - module.exit_json(**result) - - # luks remove - if conditions.luks_remove(): - if not module.check_mode: - try: - crypt.run_luks_remove(conditions.device) - except ValueError as e: - module.fail_json(msg="luks_device error: %s" % e) - result['changed'] = True - if module.check_mode: - module.exit_json(**result) - - # Success - return result - module.exit_json(**result) - - -def main(): - run_module() - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/openssh_cert.py b/lib/ansible/modules/crypto/openssh_cert.py deleted file mode 100644 index e22e27afa2..0000000000 --- a/lib/ansible/modules/crypto/openssh_cert.py +++ /dev/null @@ -1,590 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: openssh_cert -author: "David Kainz (@lolcube)" -version_added: "2.8" -short_description: Generate OpenSSH host or user certificates. -description: - - Generate and regenerate OpenSSH host or user certificates. -requirements: - - "ssh-keygen" -options: - state: - description: - - Whether the host or user certificate should exist or not, taking action if the state is different from what is stated. - type: str - default: "present" - choices: [ 'present', 'absent' ] - type: - description: - - Whether the module should generate a host or a user certificate. - - Required if I(state) is C(present). - type: str - choices: ['host', 'user'] - force: - description: - - Should the certificate be regenerated even if it already exists and is valid. - type: bool - default: false - path: - description: - - Path of the file containing the certificate. - type: path - required: true - signing_key: - description: - - The path to the private openssh key that is used for signing the public key in order to generate the certificate. - - Required if I(state) is C(present). - type: path - public_key: - description: - - The path to the public key that will be signed with the signing key in order to generate the certificate. - - Required if I(state) is C(present). - type: path - valid_from: - description: - - "The point in time the certificate is valid from. Time can be specified either as relative time or as absolute timestamp. - Time will always be interpreted as UTC. Valid formats are: C([+-]timespec | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS | YYYY-MM-DD HH:MM:SS | always) - where timespec can be an integer + C([w | d | h | m | s]) (e.g. C(+32w1d2h). - Note that if using relative time this module is NOT idempotent." - - Required if I(state) is C(present). - type: str - valid_to: - description: - - "The point in time the certificate is valid to. Time can be specified either as relative time or as absolute timestamp. - Time will always be interpreted as UTC. Valid formats are: C([+-]timespec | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS | YYYY-MM-DD HH:MM:SS | forever) - where timespec can be an integer + C([w | d | h | m | s]) (e.g. C(+32w1d2h). - Note that if using relative time this module is NOT idempotent." - - Required if I(state) is C(present). - type: str - valid_at: - description: - - "Check if the certificate is valid at a certain point in time. If it is not the certificate will be regenerated. - Time will always be interpreted as UTC. Mainly to be used with relative timespec for I(valid_from) and / or I(valid_to). - Note that if using relative time this module is NOT idempotent." - type: str - principals: - description: - - "Certificates may be limited to be valid for a set of principal (user/host) names. - By default, generated certificates are valid for all users or hosts." - type: list - elements: str - options: - description: - - "Specify certificate options when signing a key. The option that are valid for user certificates are:" - - "C(clear): Clear all enabled permissions. This is useful for clearing the default set of permissions so permissions may be added individually." - - "C(force-command=command): Forces the execution of command instead of any shell or - command specified by the user when the certificate is used for authentication." - - "C(no-agent-forwarding): Disable ssh-agent forwarding (permitted by default)." - - "C(no-port-forwarding): Disable port forwarding (permitted by default)." - - "C(no-pty Disable): PTY allocation (permitted by default)." - - "C(no-user-rc): Disable execution of C(~/.ssh/rc) by sshd (permitted by default)." - - "C(no-x11-forwarding): Disable X11 forwarding (permitted by default)" - - "C(permit-agent-forwarding): Allows ssh-agent forwarding." - - "C(permit-port-forwarding): Allows port forwarding." - - "C(permit-pty): Allows PTY allocation." - - "C(permit-user-rc): Allows execution of C(~/.ssh/rc) by sshd." - - "C(permit-x11-forwarding): Allows X11 forwarding." - - "C(source-address=address_list): Restrict the source addresses from which the certificate is considered valid. - The C(address_list) is a comma-separated list of one or more address/netmask pairs in CIDR format." - - "At present, no options are valid for host keys." - type: list - elements: str - identifier: - description: - - Specify the key identity when signing a public key. The identifier that is logged by the server when the certificate is used for authentication. - type: str - serial_number: - description: - - "Specify the certificate serial number. - The serial number is logged by the server when the certificate is used for authentication. - The certificate serial number may be used in a KeyRevocationList. - The serial number may be omitted for checks, but must be specified again for a new certificate. - Note: The default value set by ssh-keygen is 0." - type: int - -extends_documentation_fragment: files -''' - -EXAMPLES = ''' -# Generate an OpenSSH user certificate that is valid forever and for all users -- openssh_cert: - type: user - signing_key: /path/to/private_key - public_key: /path/to/public_key.pub - path: /path/to/certificate - valid_from: always - valid_to: forever - -# Generate an OpenSSH host certificate that is valid for 32 weeks from now and will be regenerated -# if it is valid for less than 2 weeks from the time the module is being run -- openssh_cert: - type: host - signing_key: /path/to/private_key - public_key: /path/to/public_key.pub - path: /path/to/certificate - valid_from: +0s - valid_to: +32w - valid_at: +2w - -# Generate an OpenSSH host certificate that is valid forever and only for example.com and examplehost -- openssh_cert: - type: host - signing_key: /path/to/private_key - public_key: /path/to/public_key.pub - path: /path/to/certificate - valid_from: always - valid_to: forever - principals: - - example.com - - examplehost - -# Generate an OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019 -- openssh_cert: - type: host - signing_key: /path/to/private_key - public_key: /path/to/public_key.pub - path: /path/to/certificate - valid_from: "2001-01-21" - valid_to: "2019-01-21" - -# Generate an OpenSSH user Certificate with clear and force-command option: -- openssh_cert: - type: user - signing_key: /path/to/private_key - public_key: /path/to/public_key.pub - path: /path/to/certificate - valid_from: always - valid_to: forever - options: - - "clear" - - "force-command=/tmp/bla/foo" - -''' - -RETURN = ''' -type: - description: type of the certificate (host or user) - returned: changed or success - type: str - sample: host -filename: - description: path to the certificate - returned: changed or success - type: str - sample: /tmp/certificate-cert.pub -info: - description: Information about the certificate. Output of C(ssh-keygen -L -f). - returned: change or success - type: list - elements: str - -''' - -import os -import errno -import re -import tempfile - -from datetime import datetime -from datetime import MINYEAR, MAXYEAR -from shutil import copy2 -from shutil import rmtree -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.crypto import convert_relative_to_datetime -from ansible.module_utils._text import to_native - - -class CertificateError(Exception): - pass - - -class Certificate(object): - - def __init__(self, module): - self.state = module.params['state'] - self.force = module.params['force'] - self.type = module.params['type'] - self.signing_key = module.params['signing_key'] - self.public_key = module.params['public_key'] - self.path = module.params['path'] - self.identifier = module.params['identifier'] - self.serial_number = module.params['serial_number'] - self.valid_from = module.params['valid_from'] - self.valid_to = module.params['valid_to'] - self.valid_at = module.params['valid_at'] - self.principals = module.params['principals'] - self.options = module.params['options'] - self.changed = False - self.check_mode = module.check_mode - self.cert_info = {} - - if self.state == 'present': - - if self.options and self.type == "host": - module.fail_json(msg="Options can only be used with user certificates.") - - if self.valid_at: - self.valid_at = self.valid_at.lstrip() - - self.valid_from = self.valid_from.lstrip() - self.valid_to = self.valid_to.lstrip() - - self.ssh_keygen = module.get_bin_path('ssh-keygen', True) - - def generate(self, module): - - if not self.is_valid(module, perms_required=False) or self.force: - args = [ - self.ssh_keygen, - '-s', self.signing_key - ] - - validity = "" - - if not (self.valid_from == "always" and self.valid_to == "forever"): - - if not self.valid_from == "always": - timeobj = self.convert_to_datetime(module, self.valid_from) - validity += ( - str(timeobj.year).zfill(4) + - str(timeobj.month).zfill(2) + - str(timeobj.day).zfill(2) + - str(timeobj.hour).zfill(2) + - str(timeobj.minute).zfill(2) + - str(timeobj.second).zfill(2) - ) - else: - validity += "19700101010101" - - validity += ":" - - if self.valid_to == "forever": - # on ssh-keygen versions that have the year 2038 bug this will cause the datetime to be 2038-01-19T04:14:07 - timeobj = datetime(MAXYEAR, 12, 31) - else: - timeobj = self.convert_to_datetime(module, self.valid_to) - - validity += ( - str(timeobj.year).zfill(4) + - str(timeobj.month).zfill(2) + - str(timeobj.day).zfill(2) + - str(timeobj.hour).zfill(2) + - str(timeobj.minute).zfill(2) + - str(timeobj.second).zfill(2) - ) - - args.extend(["-V", validity]) - - if self.type == 'host': - args.extend(['-h']) - - if self.identifier: - args.extend(['-I', self.identifier]) - else: - args.extend(['-I', ""]) - - if self.serial_number is not None: - args.extend(['-z', str(self.serial_number)]) - - if self.principals: - args.extend(['-n', ','.join(self.principals)]) - - if self.options: - for option in self.options: - args.extend(['-O']) - args.extend([option]) - - args.extend(['-P', '']) - - try: - temp_directory = tempfile.mkdtemp() - copy2(self.public_key, temp_directory) - args.extend([temp_directory + "/" + os.path.basename(self.public_key)]) - module.run_command(args, environ_update=dict(TZ="UTC"), check_rc=True) - copy2(temp_directory + "/" + os.path.splitext(os.path.basename(self.public_key))[0] + "-cert.pub", self.path) - rmtree(temp_directory, ignore_errors=True) - proc = module.run_command([self.ssh_keygen, '-L', '-f', self.path]) - self.cert_info = proc[1].split() - self.changed = True - except Exception as e: - try: - self.remove() - rmtree(temp_directory, ignore_errors=True) - except OSError as exc: - if exc.errno != errno.ENOENT: - raise CertificateError(exc) - else: - pass - module.fail_json(msg="%s" % to_native(e)) - - file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, False): - self.changed = True - - def convert_to_datetime(self, module, timestring): - - if self.is_relative(timestring): - result = convert_relative_to_datetime(timestring) - if result is None: - module.fail_json( - msg="'%s' is not a valid time format." % timestring) - else: - return result - else: - formats = ["%Y-%m-%d", - "%Y-%m-%d %H:%M:%S", - "%Y-%m-%dT%H:%M:%S", - ] - for fmt in formats: - try: - return datetime.strptime(timestring, fmt) - except ValueError: - pass - module.fail_json(msg="'%s' is not a valid time format" % timestring) - - def is_relative(self, timestr): - if timestr.startswith("+") or timestr.startswith("-"): - return True - return False - - def is_same_datetime(self, datetime_one, datetime_two): - - # This function is for backwards compatibility only because .total_seconds() is new in python2.7 - def timedelta_total_seconds(time_delta): - return (time_delta.microseconds + 0.0 + (time_delta.seconds + time_delta.days * 24 * 3600) * 10 ** 6) / 10 ** 6 - # try to use .total_ seconds() from python2.7 - try: - return (datetime_one - datetime_two).total_seconds() == 0.0 - except AttributeError: - return timedelta_total_seconds(datetime_one - datetime_two) == 0.0 - - def is_valid(self, module, perms_required=True): - - def _check_state(): - return os.path.exists(self.path) - - if _check_state(): - proc = module.run_command([self.ssh_keygen, '-L', '-f', self.path], environ_update=dict(TZ="UTC"), check_rc=False) - if proc[0] != 0: - return False - self.cert_info = proc[1].split() - principals = re.findall("(?<=Principals:)(.*)(?=Critical)", proc[1], re.S)[0].split() - principals = list(map(str.strip, principals)) - if principals == ["(none)"]: - principals = None - cert_type = re.findall("( user | host )", proc[1])[0].strip() - serial_number = re.search(r"Serial: (\d+)", proc[1]).group(1) - validity = re.findall("(from (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}) to (\\d{4}-\\d{2}-\\d{2}T\\d{2}(:\\d{2}){2}))", proc[1]) - if validity: - if validity[0][1]: - cert_valid_from = self.convert_to_datetime(module, validity[0][1]) - if self.is_same_datetime(cert_valid_from, self.convert_to_datetime(module, "1970-01-01 01:01:01")): - cert_valid_from = datetime(MINYEAR, 1, 1) - else: - cert_valid_from = datetime(MINYEAR, 1, 1) - - if validity[0][3]: - cert_valid_to = self.convert_to_datetime(module, validity[0][3]) - if self.is_same_datetime(cert_valid_to, self.convert_to_datetime(module, "2038-01-19 03:14:07")): - cert_valid_to = datetime(MAXYEAR, 12, 31) - else: - cert_valid_to = datetime(MAXYEAR, 12, 31) - else: - cert_valid_from = datetime(MINYEAR, 1, 1) - cert_valid_to = datetime(MAXYEAR, 12, 31) - else: - return False - - def _check_perms(module): - file_args = module.load_file_common_arguments(module.params) - return not module.set_fs_attributes_if_different(file_args, False) - - def _check_serial_number(): - if self.serial_number is None: - return True - return self.serial_number == int(serial_number) - - def _check_type(): - return self.type == cert_type - - def _check_principals(): - if not principals or not self.principals: - return self.principals == principals - return set(self.principals) == set(principals) - - def _check_validity(module): - if self.valid_from == "always": - earliest_time = datetime(MINYEAR, 1, 1) - elif self.is_relative(self.valid_from): - earliest_time = None - else: - earliest_time = self.convert_to_datetime(module, self.valid_from) - - if self.valid_to == "forever": - last_time = datetime(MAXYEAR, 12, 31) - elif self.is_relative(self.valid_to): - last_time = None - else: - last_time = self.convert_to_datetime(module, self.valid_to) - - if earliest_time: - if not self.is_same_datetime(earliest_time, cert_valid_from): - return False - if last_time: - if not self.is_same_datetime(last_time, cert_valid_to): - return False - - if self.valid_at: - if cert_valid_from <= self.convert_to_datetime(module, self.valid_at) <= cert_valid_to: - return True - - if earliest_time and last_time: - return True - - return False - - if perms_required and not _check_perms(module): - return False - - return _check_type() and _check_principals() and _check_validity(module) and _check_serial_number() - - def dump(self): - - """Serialize the object into a dictionary.""" - - def filter_keywords(arr, keywords): - concated = [] - string = "" - for word in arr: - if word in keywords: - concated.append(string) - string = word - else: - string += " " + word - concated.append(string) - # drop the certificate path - concated.pop(0) - return concated - - def format_cert_info(): - return filter_keywords(self.cert_info, [ - "Type:", - "Public", - "Signing", - "Key", - "Serial:", - "Valid:", - "Principals:", - "Critical", - "Extensions:"]) - - if self.state == 'present': - result = { - 'changed': self.changed, - 'type': self.type, - 'filename': self.path, - 'info': format_cert_info(), - } - else: - result = { - 'changed': self.changed, - } - - return result - - def remove(self): - """Remove the resource from the filesystem.""" - - try: - os.remove(self.path) - self.changed = True - except OSError as exc: - if exc.errno != errno.ENOENT: - raise CertificateError(exc) - else: - pass - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - state=dict(type='str', default='present', choices=['absent', 'present']), - force=dict(type='bool', default=False), - type=dict(type='str', choices=['host', 'user']), - signing_key=dict(type='path'), - public_key=dict(type='path'), - path=dict(type='path', required=True), - identifier=dict(type='str'), - serial_number=dict(type='int'), - valid_from=dict(type='str'), - valid_to=dict(type='str'), - valid_at=dict(type='str'), - principals=dict(type='list', elements='str'), - options=dict(type='list', elements='str'), - ), - supports_check_mode=True, - add_file_common_args=True, - required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])], - ) - - def isBaseDir(path): - base_dir = os.path.dirname(path) or '.' - if not os.path.isdir(base_dir): - module.fail_json( - name=base_dir, - msg='The directory %s does not exist or the file is not a directory' % base_dir - ) - if module.params['state'] == "present": - isBaseDir(module.params['signing_key']) - isBaseDir(module.params['public_key']) - - isBaseDir(module.params['path']) - - certificate = Certificate(module) - - if certificate.state == 'present': - - if module.check_mode: - certificate.changed = module.params['force'] or not certificate.is_valid(module) - else: - try: - certificate.generate(module) - except Exception as exc: - module.fail_json(msg=to_native(exc)) - - else: - - if module.check_mode: - certificate.changed = os.path.exists(module.params['path']) - if certificate.changed: - certificate.cert_info = {} - else: - try: - certificate.remove() - except Exception as exc: - module.fail_json(msg=to_native(exc)) - - result = certificate.dump() - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/openssh_keypair.py b/lib/ansible/modules/crypto/openssh_keypair.py deleted file mode 100644 index 86667f4f93..0000000000 --- a/lib/ansible/modules/crypto/openssh_keypair.py +++ /dev/null @@ -1,493 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - - -ANSIBLE_METADATA = { - 'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community' -} - -DOCUMENTATION = ''' ---- -module: openssh_keypair -author: "David Kainz (@lolcube)" -version_added: "2.8" -short_description: Generate OpenSSH private and public keys. -description: - - "This module allows one to (re)generate OpenSSH private and public keys. It uses - ssh-keygen to generate keys. One can generate C(rsa), C(dsa), C(rsa1), C(ed25519) - or C(ecdsa) private keys." -requirements: - - "ssh-keygen" -options: - state: - description: - - Whether the private and public keys should exist or not, taking action if the state is different from what is stated. - type: str - default: present - choices: [ present, absent ] - size: - description: - - "Specifies the number of bits in the private key to create. For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. - Generally, 2048 bits is considered sufficient. DSA keys must be exactly 1024 bits as specified by FIPS 186-2. - For ECDSA keys, size determines the key length by selecting from one of three elliptic curve sizes: 256, 384 or 521 bits. - Attempting to use bit lengths other than these three values for ECDSA keys will cause this module to fail. - Ed25519 keys have a fixed length and the size will be ignored." - type: int - type: - description: - - "The algorithm used to generate the SSH private key. C(rsa1) is for protocol version 1. - C(rsa1) is deprecated and may not be supported by every version of ssh-keygen." - type: str - default: rsa - choices: ['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519'] - force: - description: - - Should the key be regenerated even if it already exists - type: bool - default: false - path: - description: - - Name of the files containing the public and private key. The file containing the public key will have the extension C(.pub). - type: path - required: true - comment: - description: - - Provides a new comment to the public key. When checking if the key is in the correct state this will be ignored. - type: str - version_added: "2.9" - regenerate: - description: - - Allows to configure in which situations the module is allowed to regenerate private keys. - The module will always generate a new key if the destination file does not exist. - - By default, the key will be regenerated when it doesn't match the module's options, - except when the key cannot be read or the passphrase does not match. Please note that - this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence) - is specified. - - If set to C(never), the module will fail if the key cannot be read or the passphrase - isn't matching, and will never regenerate an existing key. - - If set to C(fail), the module will fail if the key does not correspond to the module's - options. - - If set to C(partial_idempotence), the key will be regenerated if it does not conform to - the module's options. The key is B(not) regenerated if it cannot be read (broken file), - the key is protected by an unknown passphrase, or when they key is not protected by a - passphrase, but a passphrase is specified. - - If set to C(full_idempotence), the key will be regenerated if it does not conform to the - module's options. This is also the case if the key cannot be read (broken file), the key - is protected by an unknown passphrase, or when they key is not protected by a passphrase, - but a passphrase is specified. Make sure you have a B(backup) when using this option! - - If set to C(always), the module will always regenerate the key. This is equivalent to - setting I(force) to C(yes). - - Note that adjusting the comment and the permissions can be changed without regeneration. - Therefore, even for C(never), the task can result in changed. - type: str - choices: - - never - - fail - - partial_idempotence - - full_idempotence - - always - default: partial_idempotence - version_added: '2.10' -notes: - - In case the ssh key is broken or password protected, the module will fail. Set the I(force) option to C(yes) if you want to regenerate the keypair. - -extends_documentation_fragment: files -''' - -EXAMPLES = ''' -# Generate an OpenSSH keypair with the default values (4096 bits, rsa) -- openssh_keypair: - path: /tmp/id_ssh_rsa - -# Generate an OpenSSH rsa keypair with a different size (2048 bits) -- openssh_keypair: - path: /tmp/id_ssh_rsa - size: 2048 - -# Force regenerate an OpenSSH keypair if it already exists -- openssh_keypair: - path: /tmp/id_ssh_rsa - force: True - -# Generate an OpenSSH keypair with a different algorithm (dsa) -- openssh_keypair: - path: /tmp/id_ssh_dsa - type: dsa -''' - -RETURN = ''' -size: - description: Size (in bits) of the SSH private key - returned: changed or success - type: int - sample: 4096 -type: - description: Algorithm used to generate the SSH private key - returned: changed or success - type: str - sample: rsa -filename: - description: Path to the generated SSH private key file - returned: changed or success - type: str - sample: /tmp/id_ssh_rsa -fingerprint: - description: The fingerprint of the key. - returned: changed or success - type: str - sample: SHA256:r4YCZxihVjedH2OlfjVGI6Y5xAYtdCwk8VxKyzVyYfM -public_key: - description: The public key of the generated SSH private key - returned: changed or success - type: str - sample: ssh-rsa AAAAB3Nza(...omitted...)veL4E3Xcw== test_key -comment: - description: The comment of the generated key - returned: changed or success - type: str - sample: test@comment -''' - -import os -import stat -import errno - -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native - - -class KeypairError(Exception): - pass - - -class Keypair(object): - - def __init__(self, module): - self.path = module.params['path'] - self.state = module.params['state'] - self.force = module.params['force'] - self.size = module.params['size'] - self.type = module.params['type'] - self.comment = module.params['comment'] - self.changed = False - self.check_mode = module.check_mode - self.privatekey = None - self.fingerprint = {} - self.public_key = {} - self.regenerate = module.params['regenerate'] - if self.regenerate == 'always': - self.force = True - - if self.type in ('rsa', 'rsa1'): - self.size = 4096 if self.size is None else self.size - if self.size < 1024: - module.fail_json(msg=('For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. ' - 'Attempting to use bit lengths under 1024 will cause the module to fail.')) - - if self.type == 'dsa': - self.size = 1024 if self.size is None else self.size - if self.size != 1024: - module.fail_json(msg=('DSA keys must be exactly 1024 bits as specified by FIPS 186-2.')) - - if self.type == 'ecdsa': - self.size = 256 if self.size is None else self.size - if self.size not in (256, 384, 521): - module.fail_json(msg=('For ECDSA keys, size determines the key length by selecting from ' - 'one of three elliptic curve sizes: 256, 384 or 521 bits. ' - 'Attempting to use bit lengths other than these three values for ' - 'ECDSA keys will cause this module to fail. ')) - if self.type == 'ed25519': - self.size = 256 - - def generate(self, module): - # generate a keypair - if self.force or not self.isPrivateKeyValid(module, perms_required=False): - args = [ - module.get_bin_path('ssh-keygen', True), - '-q', - '-N', '', - '-b', str(self.size), - '-t', self.type, - '-f', self.path, - ] - - if self.comment: - args.extend(['-C', self.comment]) - else: - args.extend(['-C', ""]) - - try: - if os.path.exists(self.path) and not os.access(self.path, os.W_OK): - os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR) - self.changed = True - stdin_data = None - if os.path.exists(self.path): - stdin_data = 'y' - module.run_command(args, data=stdin_data) - proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path]) - self.fingerprint = proc[1].split() - pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path]) - self.public_key = pubkey[1].strip('\n') - except Exception as e: - self.remove() - module.fail_json(msg="%s" % to_native(e)) - - elif not self.isPublicKeyValid(module, perms_required=False): - pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path]) - pubkey = pubkey[1].strip('\n') - try: - self.changed = True - with open(self.path + ".pub", "w") as pubkey_f: - pubkey_f.write(pubkey + '\n') - os.chmod(self.path + ".pub", stat.S_IWUSR + stat.S_IRUSR + stat.S_IRGRP + stat.S_IROTH) - except IOError: - module.fail_json( - msg='The public key is missing or does not match the private key. ' - 'Unable to regenerate the public key.') - self.public_key = pubkey - - if self.comment: - try: - if os.path.exists(self.path) and not os.access(self.path, os.W_OK): - os.chmod(self.path, stat.S_IWUSR + stat.S_IRUSR) - args = [module.get_bin_path('ssh-keygen', True), - '-q', '-o', '-c', '-C', self.comment, '-f', self.path] - module.run_command(args) - except IOError: - module.fail_json( - msg='Unable to update the comment for the public key.') - - file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, False): - self.changed = True - file_args['path'] = file_args['path'] + '.pub' - if module.set_fs_attributes_if_different(file_args, False): - self.changed = True - - def _check_pass_protected_or_broken_key(self, module): - key_state = module.run_command([module.get_bin_path('ssh-keygen', True), - '-P', '', '-yf', self.path], check_rc=False) - if key_state[0] == 255 or 'is not a public key file' in key_state[2]: - return True - if 'incorrect passphrase' in key_state[2] or 'load failed' in key_state[2]: - return True - return False - - def isPrivateKeyValid(self, module, perms_required=True): - - # check if the key is correct - def _check_state(): - return os.path.exists(self.path) - - if not _check_state(): - return False - - if self._check_pass_protected_or_broken_key(module): - if self.regenerate in ('full_idempotence', 'always'): - return False - module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.' - ' Will not proceed. To force regeneration, call the module with `generate`' - ' set to `full_idempotence` or `always`, or with `force=yes`.') - - proc = module.run_command([module.get_bin_path('ssh-keygen', True), '-lf', self.path], check_rc=False) - if not proc[0] == 0: - if os.path.isdir(self.path): - module.fail_json(msg='%s is a directory. Please specify a path to a file.' % (self.path)) - - if self.regenerate in ('full_idempotence', 'always'): - return False - module.fail_json(msg='Unable to read the key. The key is protected with a passphrase or broken.' - ' Will not proceed. To force regeneration, call the module with `generate`' - ' set to `full_idempotence` or `always`, or with `force=yes`.') - - fingerprint = proc[1].split() - keysize = int(fingerprint[0]) - keytype = fingerprint[-1][1:-1].lower() - - self.fingerprint = fingerprint - - if self.regenerate == 'never': - return True - - def _check_type(): - return self.type == keytype - - def _check_size(): - return self.size == keysize - - if not (_check_type() and _check_size()): - if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'): - return False - module.fail_json(msg='Key has wrong type and/or size.' - ' Will not proceed. To force regeneration, call the module with `generate`' - ' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.') - - def _check_perms(module): - file_args = module.load_file_common_arguments(module.params) - return not module.set_fs_attributes_if_different(file_args, False) - - return not perms_required or _check_perms(module) - - def isPublicKeyValid(self, module, perms_required=True): - - def _get_pubkey_content(): - if os.path.exists(self.path + ".pub"): - with open(self.path + ".pub", "r") as pubkey_f: - present_pubkey = pubkey_f.read().strip(' \n') - return present_pubkey - else: - return False - - def _parse_pubkey(pubkey_content): - if pubkey_content: - parts = pubkey_content.split(' ', 2) - if len(parts) < 2: - return False - return parts[0], parts[1], '' if len(parts) <= 2 else parts[2] - return False - - def _pubkey_valid(pubkey): - if pubkey_parts and _parse_pubkey(pubkey): - return pubkey_parts[:2] == _parse_pubkey(pubkey)[:2] - return False - - def _comment_valid(): - if pubkey_parts: - return pubkey_parts[2] == self.comment - return False - - def _check_perms(module): - file_args = module.load_file_common_arguments(module.params) - file_args['path'] = file_args['path'] + '.pub' - return not module.set_fs_attributes_if_different(file_args, False) - - pubkey_parts = _parse_pubkey(_get_pubkey_content()) - - pubkey = module.run_command([module.get_bin_path('ssh-keygen', True), '-yf', self.path]) - pubkey = pubkey[1].strip('\n') - if _pubkey_valid(pubkey): - self.public_key = pubkey - else: - return False - - if self.comment: - if not _comment_valid(): - return False - - if perms_required: - if not _check_perms(module): - return False - - return True - - def dump(self): - # return result as a dict - - """Serialize the object into a dictionary.""" - result = { - 'changed': self.changed, - 'size': self.size, - 'type': self.type, - 'filename': self.path, - # On removal this has no value - 'fingerprint': self.fingerprint[1] if self.fingerprint else '', - 'public_key': self.public_key, - 'comment': self.comment if self.comment else '', - } - - return result - - def remove(self): - """Remove the resource from the filesystem.""" - - try: - os.remove(self.path) - self.changed = True - except OSError as exc: - if exc.errno != errno.ENOENT: - raise KeypairError(exc) - else: - pass - - if os.path.exists(self.path + ".pub"): - try: - os.remove(self.path + ".pub") - self.changed = True - except OSError as exc: - if exc.errno != errno.ENOENT: - raise KeypairError(exc) - else: - pass - - -def main(): - - # Define Ansible Module - module = AnsibleModule( - argument_spec=dict( - state=dict(type='str', default='present', choices=['present', 'absent']), - size=dict(type='int'), - type=dict(type='str', default='rsa', choices=['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519']), - force=dict(type='bool', default=False), - path=dict(type='path', required=True), - comment=dict(type='str'), - regenerate=dict( - type='str', - default='partial_idempotence', - choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] - ), - ), - supports_check_mode=True, - add_file_common_args=True, - ) - - # Check if Path exists - base_dir = os.path.dirname(module.params['path']) or '.' - if not os.path.isdir(base_dir): - module.fail_json( - name=base_dir, - msg='The directory %s does not exist or the file is not a directory' % base_dir - ) - - keypair = Keypair(module) - - if keypair.state == 'present': - - if module.check_mode: - result = keypair.dump() - result['changed'] = keypair.force or not keypair.isPrivateKeyValid(module) or not keypair.isPublicKeyValid(module) - module.exit_json(**result) - - try: - keypair.generate(module) - except Exception as exc: - module.fail_json(msg=to_native(exc)) - else: - - if module.check_mode: - keypair.changed = os.path.exists(module.params['path']) - if keypair.changed: - keypair.fingerprint = {} - result = keypair.dump() - module.exit_json(**result) - - try: - keypair.remove() - except Exception as exc: - module.fail_json(msg=to_native(exc)) - - result = keypair.dump() - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/openssl_certificate.py b/lib/ansible/modules/crypto/openssl_certificate.py deleted file mode 100644 index 4bd5e5c468..0000000000 --- a/lib/ansible/modules/crypto/openssl_certificate.py +++ /dev/null @@ -1,2756 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org> -# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: openssl_certificate -version_added: "2.4" -short_description: Generate and/or check OpenSSL certificates -description: - - This module allows one to (re)generate OpenSSL certificates. - - It implements a notion of provider (ie. C(selfsigned), C(ownca), C(acme), C(assertonly), C(entrust)) - for your certificate. - - The C(assertonly) provider is intended for use cases where one is only interested in - checking properties of a supplied certificate. Please note that this provider has been - deprecated in Ansible 2.9 and will be removed in Ansible 2.13. See the examples on how - to emulate C(assertonly) usage with M(openssl_certificate_info), M(openssl_csr_info), - M(openssl_privatekey_info) and M(assert). This also allows more flexible checks than - the ones offered by the C(assertonly) provider. - - The C(ownca) provider is intended for generating OpenSSL certificate signed with your own - CA (Certificate Authority) certificate (self-signed certificate). - - Many properties that can be specified in this module are for validation of an - existing or newly generated certificate. The proper place to specify them, if you - want to receive a certificate with these properties is a CSR (Certificate Signing Request). - - "Please note that the module regenerates existing certificate if it doesn't match the module's - options, or if it seems to be corrupt. If you are concerned that this could overwrite - your existing certificate, consider using the I(backup) option." - - It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. - - If both the cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements) - cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with C(select_crypto_backend)). - Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13. -requirements: - - PyOpenSSL >= 0.15 or cryptography >= 1.6 (if using C(selfsigned) or C(assertonly) provider) - - acme-tiny >= 4.0.0 (if using the C(acme) provider) -author: - - Yanis Guenane (@Spredzy) - - Markus Teufelberger (@MarkusTeufelberger) -options: - state: - description: - - Whether the certificate should exist or not, taking action if the state is different from what is stated. - type: str - default: present - choices: [ absent, present ] - - path: - description: - - Remote absolute path where the generated certificate file should be created or is already located. - type: path - required: true - - provider: - description: - - Name of the provider to use to generate/retrieve the OpenSSL certificate. - - The C(assertonly) provider will not generate files and fail if the certificate file is missing. - - The C(assertonly) provider has been deprecated in Ansible 2.9 and will be removed in Ansible 2.13. - Please see the examples on how to emulate it with M(openssl_certificate_info), M(openssl_csr_info), - M(openssl_privatekey_info) and M(assert). - - "The C(entrust) provider was added for Ansible 2.9 and requires credentials for the - L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API." - - Required if I(state) is C(present). - type: str - choices: [ acme, assertonly, entrust, ownca, selfsigned ] - - force: - description: - - Generate the certificate, even if it already exists. - type: bool - default: no - - csr_path: - description: - - Path to the Certificate Signing Request (CSR) used to generate this certificate. - - This is not required in C(assertonly) mode. - - This is mutually exclusive with I(csr_content). - type: path - csr_content: - description: - - Content of the Certificate Signing Request (CSR) used to generate this certificate. - - This is not required in C(assertonly) mode. - - This is mutually exclusive with I(csr_path). - type: str - version_added: "2.10" - - privatekey_path: - description: - - Path to the private key to use when signing the certificate. - - This is mutually exclusive with I(privatekey_content). - type: path - privatekey_content: - description: - - Path to the private key to use when signing the certificate. - - This is mutually exclusive with I(privatekey_path). - type: str - version_added: "2.10" - - privatekey_passphrase: - description: - - The passphrase for the I(privatekey_path) resp. I(privatekey_content). - - This is required if the private key is password protected. - type: str - - selfsigned_version: - description: - - Version of the C(selfsigned) certificate. - - Nowadays it should almost always be C(3). - - This is only used by the C(selfsigned) provider. - type: int - default: 3 - version_added: "2.5" - - selfsigned_digest: - description: - - Digest algorithm to be used when self-signing the certificate. - - This is only used by the C(selfsigned) provider. - type: str - default: sha256 - - selfsigned_not_before: - description: - - The point in time the certificate is valid from. - - Time can be specified either as relative time or as absolute timestamp. - - Time will always be interpreted as UTC. - - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer - + C([w | d | h | m | s]) (e.g. C(+32w1d2h). - - Note that if using relative time this module is NOT idempotent. - - If this value is not specified, the certificate will start being valid from now. - - This is only used by the C(selfsigned) provider. - type: str - default: +0s - aliases: [ selfsigned_notBefore ] - - selfsigned_not_after: - description: - - The point in time at which the certificate stops being valid. - - Time can be specified either as relative time or as absolute timestamp. - - Time will always be interpreted as UTC. - - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer - + C([w | d | h | m | s]) (e.g. C(+32w1d2h). - - Note that if using relative time this module is NOT idempotent. - - If this value is not specified, the certificate will stop being valid 10 years from now. - - This is only used by the C(selfsigned) provider. - type: str - default: +3650d - aliases: [ selfsigned_notAfter ] - - selfsigned_create_subject_key_identifier: - description: - - Whether to create the Subject Key Identifier (SKI) from the public key. - - A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not - provide one. - - A value of C(always_create) always creates a SKI. If the CSR provides one, that one is - ignored. - - A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used. - - This is only used by the C(selfsigned) provider. - - Note that this is only supported if the C(cryptography) backend is used! - type: str - choices: [create_if_not_provided, always_create, never_create] - default: create_if_not_provided - version_added: "2.9" - - ownca_path: - description: - - Remote absolute path of the CA (Certificate Authority) certificate. - - This is only used by the C(ownca) provider. - - This is mutually exclusive with I(ownca_content). - type: path - version_added: "2.7" - ownca_content: - description: - - Content of the CA (Certificate Authority) certificate. - - This is only used by the C(ownca) provider. - - This is mutually exclusive with I(ownca_path). - type: str - version_added: "2.10" - - ownca_privatekey_path: - description: - - Path to the CA (Certificate Authority) private key to use when signing the certificate. - - This is only used by the C(ownca) provider. - - This is mutually exclusive with I(ownca_privatekey_content). - type: path - version_added: "2.7" - ownca_privatekey_content: - description: - - Path to the CA (Certificate Authority) private key to use when signing the certificate. - - This is only used by the C(ownca) provider. - - This is mutually exclusive with I(ownca_privatekey_path). - type: str - version_added: "2.10" - - ownca_privatekey_passphrase: - description: - - The passphrase for the I(ownca_privatekey_path) resp. I(ownca_privatekey_content). - - This is only used by the C(ownca) provider. - type: str - version_added: "2.7" - - ownca_digest: - description: - - The digest algorithm to be used for the C(ownca) certificate. - - This is only used by the C(ownca) provider. - type: str - default: sha256 - version_added: "2.7" - - ownca_version: - description: - - The version of the C(ownca) certificate. - - Nowadays it should almost always be C(3). - - This is only used by the C(ownca) provider. - type: int - default: 3 - version_added: "2.7" - - ownca_not_before: - description: - - The point in time the certificate is valid from. - - Time can be specified either as relative time or as absolute timestamp. - - Time will always be interpreted as UTC. - - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer - + C([w | d | h | m | s]) (e.g. C(+32w1d2h). - - Note that if using relative time this module is NOT idempotent. - - If this value is not specified, the certificate will start being valid from now. - - This is only used by the C(ownca) provider. - type: str - default: +0s - version_added: "2.7" - - ownca_not_after: - description: - - The point in time at which the certificate stops being valid. - - Time can be specified either as relative time or as absolute timestamp. - - Time will always be interpreted as UTC. - - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer - + C([w | d | h | m | s]) (e.g. C(+32w1d2h). - - Note that if using relative time this module is NOT idempotent. - - If this value is not specified, the certificate will stop being valid 10 years from now. - - This is only used by the C(ownca) provider. - type: str - default: +3650d - version_added: "2.7" - - ownca_create_subject_key_identifier: - description: - - Whether to create the Subject Key Identifier (SKI) from the public key. - - A value of C(create_if_not_provided) (default) only creates a SKI when the CSR does not - provide one. - - A value of C(always_create) always creates a SKI. If the CSR provides one, that one is - ignored. - - A value of C(never_create) never creates a SKI. If the CSR provides one, that one is used. - - This is only used by the C(ownca) provider. - - Note that this is only supported if the C(cryptography) backend is used! - type: str - choices: [create_if_not_provided, always_create, never_create] - default: create_if_not_provided - version_added: "2.9" - - ownca_create_authority_key_identifier: - description: - - Create a Authority Key Identifier from the CA's certificate. If the CSR provided - a authority key identifier, it is ignored. - - The Authority Key Identifier is generated from the CA certificate's Subject Key Identifier, - if available. If it is not available, the CA certificate's public key will be used. - - This is only used by the C(ownca) provider. - - Note that this is only supported if the C(cryptography) backend is used! - type: bool - default: yes - version_added: "2.9" - - acme_accountkey_path: - description: - - The path to the accountkey for the C(acme) provider. - - This is only used by the C(acme) provider. - type: path - - acme_challenge_path: - description: - - The path to the ACME challenge directory that is served on U(http://<HOST>:80/.well-known/acme-challenge/) - - This is only used by the C(acme) provider. - type: path - - acme_chain: - description: - - Include the intermediate certificate to the generated certificate - - This is only used by the C(acme) provider. - - Note that this is only available for older versions of C(acme-tiny). - New versions include the chain automatically, and setting I(acme_chain) to C(yes) results in an error. - type: bool - default: no - version_added: "2.5" - - acme_directory: - description: - - "The ACME directory to use. You can use any directory that supports the ACME protocol, such as Buypass or Let's Encrypt." - - "Let's Encrypt recommends using their staging server while developing jobs. U(https://letsencrypt.org/docs/staging-environment/)." - type: str - default: https://acme-v02.api.letsencrypt.org/directory - version_added: "2.10" - - signature_algorithms: - description: - - A list of algorithms that you would accept the certificate to be signed with - (e.g. ['sha256WithRSAEncryption', 'sha512WithRSAEncryption']). - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: list - elements: str - - issuer: - description: - - The key/value pairs that must be present in the issuer name field of the certificate. - - If you need to specify more than one value with the same key, use a list as value. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: dict - - issuer_strict: - description: - - If set to C(yes), the I(issuer) field must contain only these values. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: bool - default: no - version_added: "2.5" - - subject: - description: - - The key/value pairs that must be present in the subject name field of the certificate. - - If you need to specify more than one value with the same key, use a list as value. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: dict - - subject_strict: - description: - - If set to C(yes), the I(subject) field must contain only these values. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: bool - default: no - version_added: "2.5" - - has_expired: - description: - - Checks if the certificate is expired/not expired at the time the module is executed. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: bool - default: no - - version: - description: - - The version of the certificate. - - Nowadays it should almost always be 3. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: int - - valid_at: - description: - - The certificate must be valid at this point in time. - - The timestamp is formatted as an ASN.1 TIME. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: str - - invalid_at: - description: - - The certificate must be invalid at this point in time. - - The timestamp is formatted as an ASN.1 TIME. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: str - - not_before: - description: - - The certificate must start to become valid at this point in time. - - The timestamp is formatted as an ASN.1 TIME. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: str - aliases: [ notBefore ] - - not_after: - description: - - The certificate must expire at this point in time. - - The timestamp is formatted as an ASN.1 TIME. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: str - aliases: [ notAfter ] - - valid_in: - description: - - The certificate must still be valid at this relative time offset from now. - - Valid format is C([+-]timespec | number_of_seconds) where timespec can be an integer - + C([w | d | h | m | s]) (e.g. C(+32w1d2h). - - Note that if using this parameter, this module is NOT idempotent. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: str - - key_usage: - description: - - The I(key_usage) extension field must contain all these values. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: list - elements: str - aliases: [ keyUsage ] - - key_usage_strict: - description: - - If set to C(yes), the I(key_usage) extension field must contain only these values. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: bool - default: no - aliases: [ keyUsage_strict ] - - extended_key_usage: - description: - - The I(extended_key_usage) extension field must contain all these values. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: list - elements: str - aliases: [ extendedKeyUsage ] - - extended_key_usage_strict: - description: - - If set to C(yes), the I(extended_key_usage) extension field must contain only these values. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: bool - default: no - aliases: [ extendedKeyUsage_strict ] - - subject_alt_name: - description: - - The I(subject_alt_name) extension field must contain these values. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: list - elements: str - aliases: [ subjectAltName ] - - subject_alt_name_strict: - description: - - If set to C(yes), the I(subject_alt_name) extension field must contain only these values. - - This is only used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: bool - default: no - aliases: [ subjectAltName_strict ] - - 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(pyopenssl). - - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. - - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. - - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. - From that point on, only the C(cryptography) backend will be available. - type: str - default: auto - choices: [ auto, cryptography, pyopenssl ] - version_added: "2.8" - - backup: - description: - - Create a backup file including a timestamp so you can get the original - certificate back if you overwrote it with a new one by accident. - - This is not used by the C(assertonly) provider. - - This option is deprecated since Ansible 2.9 and will be removed with the C(assertonly) provider in Ansible 2.13. - For alternatives, see the example on replacing C(assertonly). - type: bool - default: no - version_added: "2.8" - - entrust_cert_type: - description: - - Specify the type of certificate requested. - - This is only used by the C(entrust) provider. - type: str - default: STANDARD_SSL - choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ] - version_added: "2.9" - - entrust_requester_email: - description: - - The email of the requester of the certificate (for tracking purposes). - - This is only used by the C(entrust) provider. - - This is required if the provider is C(entrust). - type: str - version_added: "2.9" - - entrust_requester_name: - description: - - The name of the requester of the certificate (for tracking purposes). - - This is only used by the C(entrust) provider. - - This is required if the provider is C(entrust). - type: str - version_added: "2.9" - - entrust_requester_phone: - description: - - The phone number of the requester of the certificate (for tracking purposes). - - This is only used by the C(entrust) provider. - - This is required if the provider is C(entrust). - type: str - version_added: "2.9" - - entrust_api_user: - description: - - The username for authentication to the Entrust Certificate Services (ECS) API. - - This is only used by the C(entrust) provider. - - This is required if the provider is C(entrust). - type: str - version_added: "2.9" - - entrust_api_key: - description: - - The key (password) for authentication to the Entrust Certificate Services (ECS) API. - - This is only used by the C(entrust) provider. - - This is required if the provider is C(entrust). - type: str - version_added: "2.9" - - entrust_api_client_cert_path: - description: - - The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API. - - This is only used by the C(entrust) provider. - - This is required if the provider is C(entrust). - type: path - version_added: "2.9" - - entrust_api_client_cert_key_path: - description: - - The path to the private key of the client certificate used to authenticate to the Entrust Certificate Services (ECS) API. - - This is only used by the C(entrust) provider. - - This is required if the provider is C(entrust). - type: path - version_added: "2.9" - - entrust_not_after: - description: - - The point in time at which the certificate stops being valid. - - Time can be specified either as relative time or as an absolute timestamp. - - A valid absolute time format is C(ASN.1 TIME) such as C(2019-06-18). - - A valid relative time format is C([+-]timespec) where timespec can be an integer + C([w | d | h | m | s]), such as C(+365d) or C(+32w1d2h)). - - Time will always be interpreted as UTC. - - Note that only the date (day, month, year) is supported for specifying the expiry date of the issued certificate. - - The full date-time is adjusted to EST (GMT -5:00) before issuance, which may result in a certificate with an expiration date one day - earlier than expected if a relative time is used. - - The minimum certificate lifetime is 90 days, and maximum is three years. - - If this value is not specified, the certificate will stop being valid 365 days the date of issue. - - This is only used by the C(entrust) provider. - type: str - default: +365d - version_added: "2.9" - - entrust_api_specification_path: - description: - - The path to the specification file defining the Entrust Certificate Services (ECS) API configuration. - - You can use this to keep a local copy of the specification to avoid downloading it every time the module is used. - - This is only used by the C(entrust) provider. - type: path - default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml - version_added: "2.9" - - return_content: - description: - - If set to C(yes), will return the (current or generated) certificate's content as I(certificate). - type: bool - default: no - version_added: "2.10" - -extends_documentation_fragment: files -notes: - - All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern. - - Date specified should be UTC. Minutes and seconds are mandatory. - - For security reason, when you use C(ownca) provider, you should NOT run M(openssl_certificate) on - a target machine, but on a dedicated CA machine. It is recommended not to store the CA private key - on the target machine. Once signed, the certificate can be moved to the target machine. -seealso: -- module: openssl_csr -- module: openssl_dhparam -- module: openssl_pkcs12 -- module: openssl_privatekey -- module: openssl_publickey -''' - -EXAMPLES = r''' -- name: Generate a Self Signed OpenSSL certificate - openssl_certificate: - path: /etc/ssl/crt/ansible.com.crt - privatekey_path: /etc/ssl/private/ansible.com.pem - csr_path: /etc/ssl/csr/ansible.com.csr - provider: selfsigned - -- name: Generate an OpenSSL certificate signed with your own CA certificate - openssl_certificate: - path: /etc/ssl/crt/ansible.com.crt - csr_path: /etc/ssl/csr/ansible.com.csr - ownca_path: /etc/ssl/crt/ansible_CA.crt - ownca_privatekey_path: /etc/ssl/private/ansible_CA.pem - provider: ownca - -- name: Generate a Let's Encrypt Certificate - openssl_certificate: - path: /etc/ssl/crt/ansible.com.crt - csr_path: /etc/ssl/csr/ansible.com.csr - provider: acme - acme_accountkey_path: /etc/ssl/private/ansible.com.pem - acme_challenge_path: /etc/ssl/challenges/ansible.com/ - -- name: Force (re-)generate a new Let's Encrypt Certificate - openssl_certificate: - path: /etc/ssl/crt/ansible.com.crt - csr_path: /etc/ssl/csr/ansible.com.csr - provider: acme - acme_accountkey_path: /etc/ssl/private/ansible.com.pem - acme_challenge_path: /etc/ssl/challenges/ansible.com/ - force: yes - -- name: Generate an Entrust certificate via the Entrust Certificate Services (ECS) API - openssl_certificate: - path: /etc/ssl/crt/ansible.com.crt - csr_path: /etc/ssl/csr/ansible.com.csr - provider: entrust - entrust_requester_name: Jo Doe - entrust_requester_email: jdoe@ansible.com - entrust_requester_phone: 555-555-5555 - entrust_cert_type: STANDARD_SSL - entrust_api_user: apiusername - entrust_api_key: a^lv*32!cd9LnT - entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt - entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-key.crt - entrust_api_specification_path: /etc/ssl/entrust/api-docs/cms-api-2.1.0.yaml - -# The following example shows one assertonly usage using all existing options for -# assertonly, and shows how to emulate the behavior with the openssl_certificate_info, -# openssl_csr_info, openssl_privatekey_info and assert modules: - -- openssl_certificate: - provider: assertonly - path: /etc/ssl/crt/ansible.com.crt - csr_path: /etc/ssl/csr/ansible.com.csr - privatekey_path: /etc/ssl/csr/ansible.com.key - signature_algorithms: - - sha256WithRSAEncryption - - sha512WithRSAEncryption - subject: - commonName: ansible.com - subject_strict: yes - issuer: - commonName: ansible.com - issuer_strict: yes - has_expired: no - version: 3 - key_usage: - - Data Encipherment - key_usage_strict: yes - extended_key_usage: - - DVCS - extended_key_usage_strict: yes - subject_alt_name: - - dns:ansible.com - subject_alt_name_strict: yes - not_before: 20190331202428Z - not_after: 20190413202428Z - valid_at: "+1d10h" - invalid_at: 20200331202428Z - valid_in: 10 # in ten seconds - -- openssl_certificate_info: - path: /etc/ssl/crt/ansible.com.crt - # for valid_at, invalid_at and valid_in - valid_at: - one_day_ten_hours: "+1d10h" - fixed_timestamp: 20200331202428Z - ten_seconds: "+10" - register: result - -- openssl_csr_info: - # Verifies that the CSR signature is valid; module will fail if not - path: /etc/ssl/csr/ansible.com.csr - register: result_csr - -- openssl_privatekey_info: - path: /etc/ssl/csr/ansible.com.key - register: result_privatekey - -- assert: - that: - # When private key is specified for assertonly, this will be checked: - - result.public_key == result_privatekey.public_key - # When CSR is specified for assertonly, this will be checked: - - result.public_key == result_csr.public_key - - result.subject_ordered == result_csr.subject_ordered - - result.extensions_by_oid == result_csr.extensions_by_oid - # signature_algorithms check - - "result.signature_algorithm == 'sha256WithRSAEncryption' or result.signature_algorithm == 'sha512WithRSAEncryption'" - # subject and subject_strict - - "result.subject.commonName == 'ansible.com'" - - "result.subject | length == 1" # the number must be the number of entries you check for - # issuer and issuer_strict - - "result.issuer.commonName == 'ansible.com'" - - "result.issuer | length == 1" # the number must be the number of entries you check for - # has_expired - - not result.expired - # version - - result.version == 3 - # key_usage and key_usage_strict - - "'Data Encipherment' in result.key_usage" - - "result.key_usage | length == 1" # the number must be the number of entries you check for - # extended_key_usage and extended_key_usage_strict - - "'DVCS' in result.extended_key_usage" - - "result.extended_key_usage | length == 1" # the number must be the number of entries you check for - # subject_alt_name and subject_alt_name_strict - - "'dns:ansible.com' in result.subject_alt_name" - - "result.subject_alt_name | length == 1" # the number must be the number of entries you check for - # not_before and not_after - - "result.not_before == '20190331202428Z'" - - "result.not_after == '20190413202428Z'" - # valid_at, invalid_at and valid_in - - "result.valid_at.one_day_ten_hours" # for valid_at - - "not result.valid_at.fixed_timestamp" # for invalid_at - - "result.valid_at.ten_seconds" # for valid_in - -# Examples for some checks one could use the assertonly provider for: -# (Please note that assertonly has been deprecated!) - -# How to use the assertonly provider to implement and trigger your own custom certificate generation workflow: -- name: Check if a certificate is currently still valid, ignoring failures - openssl_certificate: - path: /etc/ssl/crt/example.com.crt - provider: assertonly - has_expired: no - ignore_errors: yes - register: validity_check - -- name: Run custom task(s) to get a new, valid certificate in case the initial check failed - command: superspecialSSL recreate /etc/ssl/crt/example.com.crt - when: validity_check.failed - -- name: Check the new certificate again for validity with the same parameters, this time failing the play if it is still invalid - openssl_certificate: - path: /etc/ssl/crt/example.com.crt - provider: assertonly - has_expired: no - when: validity_check.failed - -# Some other checks that assertonly could be used for: -- name: Verify that an existing certificate was issued by the Let's Encrypt CA and is currently still valid - openssl_certificate: - path: /etc/ssl/crt/example.com.crt - provider: assertonly - issuer: - O: Let's Encrypt - has_expired: no - -- name: Ensure that a certificate uses a modern signature algorithm (no SHA1, MD5 or DSA) - openssl_certificate: - path: /etc/ssl/crt/example.com.crt - provider: assertonly - signature_algorithms: - - sha224WithRSAEncryption - - sha256WithRSAEncryption - - sha384WithRSAEncryption - - sha512WithRSAEncryption - - sha224WithECDSAEncryption - - sha256WithECDSAEncryption - - sha384WithECDSAEncryption - - sha512WithECDSAEncryption - -- name: Ensure that the existing certificate belongs to the specified private key - openssl_certificate: - path: /etc/ssl/crt/example.com.crt - privatekey_path: /etc/ssl/private/example.com.pem - provider: assertonly - -- name: Ensure that the existing certificate is still valid at the winter solstice 2017 - openssl_certificate: - path: /etc/ssl/crt/example.com.crt - provider: assertonly - valid_at: 20171221162800Z - -- name: Ensure that the existing certificate is still valid 2 weeks (1209600 seconds) from now - openssl_certificate: - path: /etc/ssl/crt/example.com.crt - provider: assertonly - valid_in: 1209600 - -- name: Ensure that the existing certificate is only used for digital signatures and encrypting other keys - openssl_certificate: - path: /etc/ssl/crt/example.com.crt - provider: assertonly - key_usage: - - digitalSignature - - keyEncipherment - key_usage_strict: true - -- name: Ensure that the existing certificate can be used for client authentication - openssl_certificate: - path: /etc/ssl/crt/example.com.crt - provider: assertonly - extended_key_usage: - - clientAuth - -- name: Ensure that the existing certificate can only be used for client authentication and time stamping - openssl_certificate: - path: /etc/ssl/crt/example.com.crt - provider: assertonly - extended_key_usage: - - clientAuth - - 1.3.6.1.5.5.7.3.8 - extended_key_usage_strict: true - -- name: Ensure that the existing certificate has a certain domain in its subjectAltName - openssl_certificate: - path: /etc/ssl/crt/example.com.crt - provider: assertonly - subject_alt_name: - - www.example.com - - test.example.com -''' - -RETURN = r''' -filename: - description: Path to the generated certificate. - returned: changed or success - type: str - sample: /etc/ssl/crt/www.ansible.com.crt -backup_file: - description: Name of backup file created. - returned: changed and if I(backup) is C(yes) - type: str - sample: /path/to/www.ansible.com.crt.2019-03-09@11:22~ -certificate: - description: The (current or generated) certificate's content. - returned: if I(state) is C(present) and I(return_content) is C(yes) - type: str - version_added: "2.10" -''' - - -from random import randint -import abc -import datetime -import time -import os -import tempfile -import traceback -from distutils.version import LooseVersion - -from ansible.module_utils import crypto as crypto_utils -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native, to_bytes, to_text -from ansible.module_utils.compat import ipaddress as compat_ipaddress -from ansible.module_utils.ecs.api import ECSClient, RestOperationException, SessionConfigurationException - -MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' -MINIMAL_PYOPENSSL_VERSION = '0.15' - -PYOPENSSL_IMP_ERR = None -try: - import OpenSSL - from OpenSSL import crypto - PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) -except ImportError: - PYOPENSSL_IMP_ERR = traceback.format_exc() - PYOPENSSL_FOUND = False -else: - PYOPENSSL_FOUND = True - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - from cryptography import x509 - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives.serialization import Encoding - from cryptography.x509 import NameAttribute, Name - from cryptography.x509.oid import NameOID - CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - - -class CertificateError(crypto_utils.OpenSSLObjectError): - pass - - -class Certificate(crypto_utils.OpenSSLObject): - - def __init__(self, module, backend): - super(Certificate, self).__init__( - module.params['path'], - module.params['state'], - module.params['force'], - module.check_mode - ) - - self.provider = module.params['provider'] - self.privatekey_path = module.params['privatekey_path'] - self.privatekey_content = module.params['privatekey_content'] - if self.privatekey_content is not None: - self.privatekey_content = self.privatekey_content.encode('utf-8') - self.privatekey_passphrase = module.params['privatekey_passphrase'] - self.csr_path = module.params['csr_path'] - self.csr_content = module.params['csr_content'] - if self.csr_content is not None: - self.csr_content = self.csr_content.encode('utf-8') - self.cert = None - self.privatekey = None - self.csr = None - self.backend = backend - self.module = module - self.return_content = module.params['return_content'] - - # The following are default values which make sure check() works as - # before if providers do not explicitly change these properties. - self.create_subject_key_identifier = 'never_create' - self.create_authority_key_identifier = False - - self.backup = module.params['backup'] - self.backup_file = None - - def _validate_privatekey(self): - if self.backend == 'pyopenssl': - ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD) - ctx.use_privatekey(self.privatekey) - ctx.use_certificate(self.cert) - try: - ctx.check_privatekey() - return True - except OpenSSL.SSL.Error: - return False - elif self.backend == 'cryptography': - return crypto_utils.cryptography_compare_public_keys(self.cert.public_key(), self.privatekey.public_key()) - - def _validate_csr(self): - if self.backend == 'pyopenssl': - # Verify that CSR is signed by certificate's private key - try: - self.csr.verify(self.cert.get_pubkey()) - except OpenSSL.crypto.Error: - return False - # Check subject - if self.csr.get_subject() != self.cert.get_subject(): - return False - # Check extensions - csr_extensions = self.csr.get_extensions() - cert_extension_count = self.cert.get_extension_count() - if len(csr_extensions) != cert_extension_count: - return False - for extension_number in range(0, cert_extension_count): - cert_extension = self.cert.get_extension(extension_number) - csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions) - if cert_extension.get_data() != list(csr_extension)[0].get_data(): - return False - return True - elif self.backend == 'cryptography': - # Verify that CSR is signed by certificate's private key - if not self.csr.is_signature_valid: - return False - if not crypto_utils.cryptography_compare_public_keys(self.csr.public_key(), self.cert.public_key()): - return False - # Check subject - if self.csr.subject != self.cert.subject: - return False - # Check extensions - cert_exts = list(self.cert.extensions) - csr_exts = list(self.csr.extensions) - if self.create_subject_key_identifier != 'never_create': - # Filter out SubjectKeyIdentifier extension before comparison - cert_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), cert_exts)) - csr_exts = list(filter(lambda x: not isinstance(x.value, x509.SubjectKeyIdentifier), csr_exts)) - if self.create_authority_key_identifier: - # Filter out AuthorityKeyIdentifier extension before comparison - cert_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), cert_exts)) - csr_exts = list(filter(lambda x: not isinstance(x.value, x509.AuthorityKeyIdentifier), csr_exts)) - if len(cert_exts) != len(csr_exts): - return False - for cert_ext in cert_exts: - try: - csr_ext = self.csr.extensions.get_extension_for_oid(cert_ext.oid) - if cert_ext != csr_ext: - return False - except cryptography.x509.ExtensionNotFound as dummy: - return False - return True - - def remove(self, module): - if self.backup: - self.backup_file = module.backup_local(self.path) - super(Certificate, self).remove(module) - - def check(self, module, perms_required=True): - """Ensure the resource is in its desired state.""" - - state_and_perms = super(Certificate, self).check(module, perms_required) - - if not state_and_perms: - return False - - try: - self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) - except Exception as dummy: - return False - - if self.privatekey_path or self.privatekey_content: - try: - self.privatekey = crypto_utils.load_privatekey( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase, - backend=self.backend - ) - except crypto_utils.OpenSSLBadPassphraseError as exc: - raise CertificateError(exc) - if not self._validate_privatekey(): - return False - - if self.csr_path or self.csr_content: - self.csr = crypto_utils.load_certificate_request( - path=self.csr_path, - content=self.csr_content, - backend=self.backend - ) - if not self._validate_csr(): - return False - - # Check SubjectKeyIdentifier - if self.backend == 'cryptography' and self.create_subject_key_identifier != 'never_create': - # Get hold of certificate's SKI - try: - ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) - except cryptography.x509.ExtensionNotFound as dummy: - return False - # Get hold of CSR's SKI for 'create_if_not_provided' - csr_ext = None - if self.create_subject_key_identifier == 'create_if_not_provided': - try: - csr_ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) - except cryptography.x509.ExtensionNotFound as dummy: - pass - if csr_ext is None: - # If CSR had no SKI, or we chose to ignore it ('always_create'), compare with created SKI - if ext.value.digest != x509.SubjectKeyIdentifier.from_public_key(self.cert.public_key()).digest: - return False - else: - # If CSR had SKI and we didn't ignore it ('create_if_not_provided'), compare SKIs - if ext.value.digest != csr_ext.value.digest: - return False - - return True - - -class CertificateAbsent(Certificate): - def __init__(self, module): - super(CertificateAbsent, self).__init__(module, 'cryptography') # backend doesn't matter - - def generate(self, module): - pass - - def dump(self, check_mode=False): - # Use only for absent - - result = { - 'changed': self.changed, - 'filename': self.path, - 'privatekey': self.privatekey_path, - 'csr': self.csr_path - } - if self.backup_file: - result['backup_file'] = self.backup_file - if self.return_content: - result['certificate'] = None - - return result - - -class SelfSignedCertificateCryptography(Certificate): - """Generate the self-signed certificate, using the cryptography backend""" - def __init__(self, module): - super(SelfSignedCertificateCryptography, self).__init__(module, 'cryptography') - self.create_subject_key_identifier = module.params['selfsigned_create_subject_key_identifier'] - self.notBefore = crypto_utils.get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) - self.notAfter = crypto_utils.get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend) - self.digest = crypto_utils.select_message_digest(module.params['selfsigned_digest']) - self.version = module.params['selfsigned_version'] - self.serial_number = x509.random_serial_number() - - if self.csr_content is None and not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file {0} does not exist'.format(self.csr_path) - ) - if self.privatekey_content is None and not os.path.exists(self.privatekey_path): - raise CertificateError( - 'The private key file {0} does not exist'.format(self.privatekey_path) - ) - - self.csr = crypto_utils.load_certificate_request( - path=self.csr_path, - content=self.csr_content, - backend=self.backend - ) - self._module = module - - try: - self.privatekey = crypto_utils.load_privatekey( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase, - backend=self.backend - ) - except crypto_utils.OpenSSLBadPassphraseError as exc: - module.fail_json(msg=to_native(exc)) - - if crypto_utils.cryptography_key_needs_digest_for_signing(self.privatekey): - if self.digest is None: - raise CertificateError( - 'The digest %s is not supported with the cryptography backend' % module.params['selfsigned_digest'] - ) - else: - self.digest = None - - def generate(self, module): - if self.privatekey_content is None and not os.path.exists(self.privatekey_path): - raise CertificateError( - 'The private key %s does not exist' % self.privatekey_path - ) - if self.csr_content is None and not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file %s does not exist' % self.csr_path - ) - if not self.check(module, perms_required=False) or self.force: - try: - cert_builder = x509.CertificateBuilder() - cert_builder = cert_builder.subject_name(self.csr.subject) - cert_builder = cert_builder.issuer_name(self.csr.subject) - cert_builder = cert_builder.serial_number(self.serial_number) - cert_builder = cert_builder.not_valid_before(self.notBefore) - cert_builder = cert_builder.not_valid_after(self.notAfter) - cert_builder = cert_builder.public_key(self.privatekey.public_key()) - has_ski = False - for extension in self.csr.extensions: - if isinstance(extension.value, x509.SubjectKeyIdentifier): - if self.create_subject_key_identifier == 'always_create': - continue - has_ski = True - cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical) - if not has_ski and self.create_subject_key_identifier != 'never_create': - cert_builder = cert_builder.add_extension( - x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()), - critical=False - ) - except ValueError as e: - raise CertificateError(str(e)) - - try: - certificate = cert_builder.sign( - private_key=self.privatekey, algorithm=self.digest, - backend=default_backend() - ) - except TypeError as e: - if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None: - module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') - raise - - self.cert = certificate - - if self.backup: - self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, certificate.public_bytes(Encoding.PEM)) - self.changed = True - else: - self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) - - file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, False): - self.changed = True - - def dump(self, check_mode=False): - - result = { - 'changed': self.changed, - 'filename': self.path, - 'privatekey': self.privatekey_path, - 'csr': self.csr_path - } - if self.backup_file: - result['backup_file'] = self.backup_file - if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - result['certificate'] = content.decode('utf-8') if content else None - - if check_mode: - result.update({ - 'notBefore': self.notBefore.strftime("%Y%m%d%H%M%SZ"), - 'notAfter': self.notAfter.strftime("%Y%m%d%H%M%SZ"), - 'serial_number': self.serial_number, - }) - else: - result.update({ - 'notBefore': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"), - 'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"), - 'serial_number': self.cert.serial_number, - }) - - return result - - -class SelfSignedCertificate(Certificate): - """Generate the self-signed certificate.""" - - def __init__(self, module): - super(SelfSignedCertificate, self).__init__(module, 'pyopenssl') - if module.params['selfsigned_create_subject_key_identifier'] != 'create_if_not_provided': - module.fail_json(msg='selfsigned_create_subject_key_identifier cannot be used with the pyOpenSSL backend!') - self.notBefore = crypto_utils.get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) - self.notAfter = crypto_utils.get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend) - self.digest = module.params['selfsigned_digest'] - self.version = module.params['selfsigned_version'] - self.serial_number = randint(1000, 99999) - - if self.csr_content is None and not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file {0} does not exist'.format(self.csr_path) - ) - if self.privatekey_content is None and not os.path.exists(self.privatekey_path): - raise CertificateError( - 'The private key file {0} does not exist'.format(self.privatekey_path) - ) - - self.csr = crypto_utils.load_certificate_request( - path=self.csr_path, - content=self.csr_content, - ) - try: - self.privatekey = crypto_utils.load_privatekey( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase, - ) - except crypto_utils.OpenSSLBadPassphraseError as exc: - module.fail_json(msg=str(exc)) - - def generate(self, module): - - if self.privatekey_content is None and not os.path.exists(self.privatekey_path): - raise CertificateError( - 'The private key %s does not exist' % self.privatekey_path - ) - - if self.csr_content is None and not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file %s does not exist' % self.csr_path - ) - - if not self.check(module, perms_required=False) or self.force: - cert = crypto.X509() - cert.set_serial_number(self.serial_number) - cert.set_notBefore(to_bytes(self.notBefore)) - cert.set_notAfter(to_bytes(self.notAfter)) - cert.set_subject(self.csr.get_subject()) - cert.set_issuer(self.csr.get_subject()) - cert.set_version(self.version - 1) - cert.set_pubkey(self.csr.get_pubkey()) - cert.add_extensions(self.csr.get_extensions()) - - cert.sign(self.privatekey, self.digest) - self.cert = cert - - if self.backup: - self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)) - self.changed = True - - file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, False): - self.changed = True - - def dump(self, check_mode=False): - - result = { - 'changed': self.changed, - 'filename': self.path, - 'privatekey': self.privatekey_path, - 'csr': self.csr_path - } - if self.backup_file: - result['backup_file'] = self.backup_file - if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - result['certificate'] = content.decode('utf-8') if content else None - - if check_mode: - result.update({ - 'notBefore': self.notBefore, - 'notAfter': self.notAfter, - 'serial_number': self.serial_number, - }) - else: - result.update({ - 'notBefore': self.cert.get_notBefore(), - 'notAfter': self.cert.get_notAfter(), - 'serial_number': self.cert.get_serial_number(), - }) - - return result - - -class OwnCACertificateCryptography(Certificate): - """Generate the own CA certificate. Using the cryptography backend""" - def __init__(self, module): - super(OwnCACertificateCryptography, self).__init__(module, 'cryptography') - self.create_subject_key_identifier = module.params['ownca_create_subject_key_identifier'] - self.create_authority_key_identifier = module.params['ownca_create_authority_key_identifier'] - self.notBefore = crypto_utils.get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) - self.notAfter = crypto_utils.get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) - self.digest = crypto_utils.select_message_digest(module.params['ownca_digest']) - self.version = module.params['ownca_version'] - self.serial_number = x509.random_serial_number() - self.ca_cert_path = module.params['ownca_path'] - self.ca_cert_content = module.params['ownca_content'] - if self.ca_cert_content is not None: - self.ca_cert_content = self.ca_cert_content.encode('utf-8') - self.ca_privatekey_path = module.params['ownca_privatekey_path'] - self.ca_privatekey_content = module.params['ownca_privatekey_content'] - if self.ca_privatekey_content is not None: - self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8') - self.ca_privatekey_passphrase = module.params['ownca_privatekey_passphrase'] - - if self.csr_content is None and not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file {0} does not exist'.format(self.csr_path) - ) - if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path): - raise CertificateError( - 'The CA certificate file {0} does not exist'.format(self.ca_cert_path) - ) - if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path): - raise CertificateError( - 'The CA private key file {0} does not exist'.format(self.ca_privatekey_path) - ) - - self.csr = crypto_utils.load_certificate_request( - path=self.csr_path, - content=self.csr_content, - backend=self.backend - ) - self.ca_cert = crypto_utils.load_certificate( - path=self.ca_cert_path, - content=self.ca_cert_content, - backend=self.backend - ) - try: - self.ca_private_key = crypto_utils.load_privatekey( - path=self.ca_privatekey_path, - content=self.ca_privatekey_content, - passphrase=self.ca_privatekey_passphrase, - backend=self.backend - ) - except crypto_utils.OpenSSLBadPassphraseError as exc: - module.fail_json(msg=str(exc)) - - if crypto_utils.cryptography_key_needs_digest_for_signing(self.ca_private_key): - if self.digest is None: - raise CertificateError( - 'The digest %s is not supported with the cryptography backend' % module.params['ownca_digest'] - ) - else: - self.digest = None - - def generate(self, module): - - if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path): - raise CertificateError( - 'The CA certificate %s does not exist' % self.ca_cert_path - ) - - if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path): - raise CertificateError( - 'The CA private key %s does not exist' % self.ca_privatekey_path - ) - - if self.csr_content is None and not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file %s does not exist' % self.csr_path - ) - - if not self.check(module, perms_required=False) or self.force: - cert_builder = x509.CertificateBuilder() - cert_builder = cert_builder.subject_name(self.csr.subject) - cert_builder = cert_builder.issuer_name(self.ca_cert.subject) - cert_builder = cert_builder.serial_number(self.serial_number) - cert_builder = cert_builder.not_valid_before(self.notBefore) - cert_builder = cert_builder.not_valid_after(self.notAfter) - cert_builder = cert_builder.public_key(self.csr.public_key()) - has_ski = False - for extension in self.csr.extensions: - if isinstance(extension.value, x509.SubjectKeyIdentifier): - if self.create_subject_key_identifier == 'always_create': - continue - has_ski = True - if self.create_authority_key_identifier and isinstance(extension.value, x509.AuthorityKeyIdentifier): - continue - cert_builder = cert_builder.add_extension(extension.value, critical=extension.critical) - if not has_ski and self.create_subject_key_identifier != 'never_create': - cert_builder = cert_builder.add_extension( - x509.SubjectKeyIdentifier.from_public_key(self.csr.public_key()), - critical=False - ) - if self.create_authority_key_identifier: - try: - ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) - cert_builder = cert_builder.add_extension( - x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value) - if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else - x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext), - critical=False - ) - except cryptography.x509.ExtensionNotFound: - cert_builder = cert_builder.add_extension( - x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()), - critical=False - ) - - try: - certificate = cert_builder.sign( - private_key=self.ca_private_key, algorithm=self.digest, - backend=default_backend() - ) - except TypeError as e: - if str(e) == 'Algorithm must be a registered hash algorithm.' and self.digest is None: - module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') - raise - - self.cert = certificate - - if self.backup: - self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, certificate.public_bytes(Encoding.PEM)) - self.changed = True - else: - self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) - - file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, False): - self.changed = True - - def check(self, module, perms_required=True): - """Ensure the resource is in its desired state.""" - - if not super(OwnCACertificateCryptography, self).check(module, perms_required): - return False - - # Check AuthorityKeyIdentifier - if self.create_authority_key_identifier: - try: - ext = self.ca_cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) - expected_ext = ( - x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext.value) - if CRYPTOGRAPHY_VERSION >= LooseVersion('2.7') else - x509.AuthorityKeyIdentifier.from_issuer_subject_key_identifier(ext) - ) - except cryptography.x509.ExtensionNotFound: - expected_ext = x509.AuthorityKeyIdentifier.from_issuer_public_key(self.ca_cert.public_key()) - try: - ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) - if ext.value != expected_ext: - return False - except cryptography.x509.ExtensionNotFound as dummy: - return False - - return True - - def dump(self, check_mode=False): - - result = { - 'changed': self.changed, - 'filename': self.path, - 'privatekey': self.privatekey_path, - 'csr': self.csr_path, - 'ca_cert': self.ca_cert_path, - 'ca_privatekey': self.ca_privatekey_path - } - if self.backup_file: - result['backup_file'] = self.backup_file - if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - result['certificate'] = content.decode('utf-8') if content else None - - if check_mode: - result.update({ - 'notBefore': self.notBefore.strftime("%Y%m%d%H%M%SZ"), - 'notAfter': self.notAfter.strftime("%Y%m%d%H%M%SZ"), - 'serial_number': self.serial_number, - }) - else: - result.update({ - 'notBefore': self.cert.not_valid_before.strftime("%Y%m%d%H%M%SZ"), - 'notAfter': self.cert.not_valid_after.strftime("%Y%m%d%H%M%SZ"), - 'serial_number': self.cert.serial_number, - }) - - return result - - -class OwnCACertificate(Certificate): - """Generate the own CA certificate.""" - - def __init__(self, module): - super(OwnCACertificate, self).__init__(module, 'pyopenssl') - self.notBefore = crypto_utils.get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) - self.notAfter = crypto_utils.get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) - self.digest = module.params['ownca_digest'] - self.version = module.params['ownca_version'] - self.serial_number = randint(1000, 99999) - if module.params['ownca_create_subject_key_identifier'] != 'create_if_not_provided': - module.fail_json(msg='ownca_create_subject_key_identifier cannot be used with the pyOpenSSL backend!') - if module.params['ownca_create_authority_key_identifier']: - module.warn('ownca_create_authority_key_identifier is ignored by the pyOpenSSL backend!') - self.ca_cert_path = module.params['ownca_path'] - self.ca_cert_content = module.params['ownca_content'] - if self.ca_cert_content is not None: - self.ca_cert_content = self.ca_cert_content.encode('utf-8') - self.ca_privatekey_path = module.params['ownca_privatekey_path'] - self.ca_privatekey_content = module.params['ownca_privatekey_content'] - if self.ca_privatekey_content is not None: - self.ca_privatekey_content = self.ca_privatekey_content.encode('utf-8') - self.ca_privatekey_passphrase = module.params['ownca_privatekey_passphrase'] - - if self.csr_content is None and not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file {0} does not exist'.format(self.csr_path) - ) - if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path): - raise CertificateError( - 'The CA certificate file {0} does not exist'.format(self.ca_cert_path) - ) - if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path): - raise CertificateError( - 'The CA private key file {0} does not exist'.format(self.ca_privatekey_path) - ) - - self.csr = crypto_utils.load_certificate_request( - path=self.csr_path, - content=self.csr_content, - ) - self.ca_cert = crypto_utils.load_certificate( - path=self.ca_cert_path, - content=self.ca_cert_content, - ) - try: - self.ca_privatekey = crypto_utils.load_privatekey( - path=self.ca_privatekey_path, - content=self.ca_privatekey_content, - passphrase=self.ca_privatekey_passphrase - ) - except crypto_utils.OpenSSLBadPassphraseError as exc: - module.fail_json(msg=str(exc)) - - def generate(self, module): - - if self.ca_cert_content is None and not os.path.exists(self.ca_cert_path): - raise CertificateError( - 'The CA certificate %s does not exist' % self.ca_cert_path - ) - - if self.ca_privatekey_content is None and not os.path.exists(self.ca_privatekey_path): - raise CertificateError( - 'The CA private key %s does not exist' % self.ca_privatekey_path - ) - - if self.csr_content is None and not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file %s does not exist' % self.csr_path - ) - - if not self.check(module, perms_required=False) or self.force: - cert = crypto.X509() - cert.set_serial_number(self.serial_number) - cert.set_notBefore(to_bytes(self.notBefore)) - cert.set_notAfter(to_bytes(self.notAfter)) - cert.set_subject(self.csr.get_subject()) - cert.set_issuer(self.ca_cert.get_subject()) - cert.set_version(self.version - 1) - cert.set_pubkey(self.csr.get_pubkey()) - cert.add_extensions(self.csr.get_extensions()) - - cert.sign(self.ca_privatekey, self.digest) - self.cert = cert - - if self.backup: - self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)) - self.changed = True - - file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, False): - self.changed = True - - def dump(self, check_mode=False): - - result = { - 'changed': self.changed, - 'filename': self.path, - 'privatekey': self.privatekey_path, - 'csr': self.csr_path, - 'ca_cert': self.ca_cert_path, - 'ca_privatekey': self.ca_privatekey_path - } - if self.backup_file: - result['backup_file'] = self.backup_file - if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - result['certificate'] = content.decode('utf-8') if content else None - - if check_mode: - result.update({ - 'notBefore': self.notBefore, - 'notAfter': self.notAfter, - 'serial_number': self.serial_number, - }) - else: - result.update({ - 'notBefore': self.cert.get_notBefore(), - 'notAfter': self.cert.get_notAfter(), - 'serial_number': self.cert.get_serial_number(), - }) - - return result - - -def compare_sets(subset, superset, equality=False): - if equality: - return set(subset) == set(superset) - else: - return all(x in superset for x in subset) - - -def compare_dicts(subset, superset, equality=False): - if equality: - return subset == superset - else: - return all(superset.get(x) == v for x, v in subset.items()) - - -NO_EXTENSION = 'no extension' - - -class AssertOnlyCertificateBase(Certificate): - - def __init__(self, module, backend): - super(AssertOnlyCertificateBase, self).__init__(module, backend) - - self.signature_algorithms = module.params['signature_algorithms'] - if module.params['subject']: - self.subject = crypto_utils.parse_name_field(module.params['subject']) - else: - self.subject = [] - self.subject_strict = module.params['subject_strict'] - if module.params['issuer']: - self.issuer = crypto_utils.parse_name_field(module.params['issuer']) - else: - self.issuer = [] - self.issuer_strict = module.params['issuer_strict'] - self.has_expired = module.params['has_expired'] - self.version = module.params['version'] - self.key_usage = module.params['key_usage'] - self.key_usage_strict = module.params['key_usage_strict'] - self.extended_key_usage = module.params['extended_key_usage'] - self.extended_key_usage_strict = module.params['extended_key_usage_strict'] - self.subject_alt_name = module.params['subject_alt_name'] - self.subject_alt_name_strict = module.params['subject_alt_name_strict'] - self.not_before = module.params['not_before'] - self.not_after = module.params['not_after'] - self.valid_at = module.params['valid_at'] - self.invalid_at = module.params['invalid_at'] - self.valid_in = module.params['valid_in'] - if self.valid_in and not self.valid_in.startswith("+") and not self.valid_in.startswith("-"): - try: - int(self.valid_in) - except ValueError: - module.fail_json(msg='The supplied value for "valid_in" (%s) is not an integer or a valid timespec' % self.valid_in) - self.valid_in = "+" + self.valid_in + "s" - - # Load objects - self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) - if self.privatekey_path is not None or self.privatekey_content is not None: - try: - self.privatekey = crypto_utils.load_privatekey( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase, - backend=self.backend - ) - except crypto_utils.OpenSSLBadPassphraseError as exc: - raise CertificateError(exc) - if self.csr_path is not None or self.csr_content is not None: - self.csr = crypto_utils.load_certificate_request( - path=self.csr_path, - content=self.csr_content, - backend=self.backend - ) - - @abc.abstractmethod - def _validate_privatekey(self): - pass - - @abc.abstractmethod - def _validate_csr_signature(self): - pass - - @abc.abstractmethod - def _validate_csr_subject(self): - pass - - @abc.abstractmethod - def _validate_csr_extensions(self): - pass - - @abc.abstractmethod - def _validate_signature_algorithms(self): - pass - - @abc.abstractmethod - def _validate_subject(self): - pass - - @abc.abstractmethod - def _validate_issuer(self): - pass - - @abc.abstractmethod - def _validate_has_expired(self): - pass - - @abc.abstractmethod - def _validate_version(self): - pass - - @abc.abstractmethod - def _validate_key_usage(self): - pass - - @abc.abstractmethod - def _validate_extended_key_usage(self): - pass - - @abc.abstractmethod - def _validate_subject_alt_name(self): - pass - - @abc.abstractmethod - def _validate_not_before(self): - pass - - @abc.abstractmethod - def _validate_not_after(self): - pass - - @abc.abstractmethod - def _validate_valid_at(self): - pass - - @abc.abstractmethod - def _validate_invalid_at(self): - pass - - @abc.abstractmethod - def _validate_valid_in(self): - pass - - def assertonly(self, module): - messages = [] - if self.privatekey_path is not None or self.privatekey_content is not None: - if not self._validate_privatekey(): - messages.append( - 'Certificate %s and private key %s do not match' % - (self.path, self.privatekey_path or '(provided in module options)') - ) - - if self.csr_path is not None or self.csr_content is not None: - if not self._validate_csr_signature(): - messages.append( - 'Certificate %s and CSR %s do not match: private key mismatch' % - (self.path, self.csr_path or '(provided in module options)') - ) - if not self._validate_csr_subject(): - messages.append( - 'Certificate %s and CSR %s do not match: subject mismatch' % - (self.path, self.csr_path or '(provided in module options)') - ) - if not self._validate_csr_extensions(): - messages.append( - 'Certificate %s and CSR %s do not match: extensions mismatch' % - (self.path, self.csr_path or '(provided in module options)') - ) - - if self.signature_algorithms is not None: - wrong_alg = self._validate_signature_algorithms() - if wrong_alg: - messages.append( - 'Invalid signature algorithm (got %s, expected one of %s)' % - (wrong_alg, self.signature_algorithms) - ) - - if self.subject is not None: - failure = self._validate_subject() - if failure: - dummy, cert_subject = failure - messages.append( - 'Invalid subject component (got %s, expected all of %s to be present)' % - (cert_subject, self.subject) - ) - - if self.issuer is not None: - failure = self._validate_issuer() - if failure: - dummy, cert_issuer = failure - messages.append( - 'Invalid issuer component (got %s, expected all of %s to be present)' % (cert_issuer, self.issuer) - ) - - if self.has_expired is not None: - cert_expired = self._validate_has_expired() - if cert_expired != self.has_expired: - messages.append( - 'Certificate expiration check failed (certificate expiration is %s, expected %s)' % - (cert_expired, self.has_expired) - ) - - if self.version is not None: - cert_version = self._validate_version() - if cert_version != self.version: - messages.append( - 'Invalid certificate version number (got %s, expected %s)' % - (cert_version, self.version) - ) - - if self.key_usage is not None: - failure = self._validate_key_usage() - if failure == NO_EXTENSION: - messages.append('Found no keyUsage extension') - elif failure: - dummy, cert_key_usage = failure - messages.append( - 'Invalid keyUsage components (got %s, expected all of %s to be present)' % - (cert_key_usage, self.key_usage) - ) - - if self.extended_key_usage is not None: - failure = self._validate_extended_key_usage() - if failure == NO_EXTENSION: - messages.append('Found no extendedKeyUsage extension') - elif failure: - dummy, ext_cert_key_usage = failure - messages.append( - 'Invalid extendedKeyUsage component (got %s, expected all of %s to be present)' % (ext_cert_key_usage, self.extended_key_usage) - ) - - if self.subject_alt_name is not None: - failure = self._validate_subject_alt_name() - if failure == NO_EXTENSION: - messages.append('Found no subjectAltName extension') - elif failure: - dummy, cert_san = failure - messages.append( - 'Invalid subjectAltName component (got %s, expected all of %s to be present)' % - (cert_san, self.subject_alt_name) - ) - - if self.not_before is not None: - cert_not_valid_before = self._validate_not_before() - if cert_not_valid_before != crypto_utils.get_relative_time_option(self.not_before, 'not_before', backend=self.backend): - messages.append( - 'Invalid not_before component (got %s, expected %s to be present)' % - (cert_not_valid_before, self.not_before) - ) - - if self.not_after is not None: - cert_not_valid_after = self._validate_not_after() - if cert_not_valid_after != crypto_utils.get_relative_time_option(self.not_after, 'not_after', backend=self.backend): - messages.append( - 'Invalid not_after component (got %s, expected %s to be present)' % - (cert_not_valid_after, self.not_after) - ) - - if self.valid_at is not None: - not_before, valid_at, not_after = self._validate_valid_at() - if not (not_before <= valid_at <= not_after): - messages.append( - 'Certificate is not valid for the specified date (%s) - not_before: %s - not_after: %s' % - (self.valid_at, not_before, not_after) - ) - - if self.invalid_at is not None: - not_before, invalid_at, not_after = self._validate_invalid_at() - if not_before <= invalid_at <= not_after: - messages.append( - 'Certificate is not invalid for the specified date (%s) - not_before: %s - not_after: %s' % - (self.invalid_at, not_before, not_after) - ) - - if self.valid_in is not None: - not_before, valid_in, not_after = self._validate_valid_in() - if not not_before <= valid_in <= not_after: - messages.append( - 'Certificate is not valid in %s from now (that would be %s) - not_before: %s - not_after: %s' % - (self.valid_in, valid_in, not_before, not_after) - ) - return messages - - def generate(self, module): - """Don't generate anything - only assert""" - messages = self.assertonly(module) - if messages: - module.fail_json(msg=' | '.join(messages)) - - def check(self, module, perms_required=False): - """Ensure the resource is in its desired state.""" - messages = self.assertonly(module) - return len(messages) == 0 - - def dump(self, check_mode=False): - result = { - 'changed': self.changed, - 'filename': self.path, - 'privatekey': self.privatekey_path, - 'csr': self.csr_path, - } - if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - result['certificate'] = content.decode('utf-8') if content else None - return result - - -class AssertOnlyCertificateCryptography(AssertOnlyCertificateBase): - """Validate the supplied cert, using the cryptography backend""" - def __init__(self, module): - super(AssertOnlyCertificateCryptography, self).__init__(module, 'cryptography') - - def _validate_privatekey(self): - return crypto_utils.cryptography_compare_public_keys(self.cert.public_key(), self.privatekey.public_key()) - - def _validate_csr_signature(self): - if not self.csr.is_signature_valid: - return False - return crypto_utils.cryptography_compare_public_keys(self.csr.public_key(), self.cert.public_key()) - - def _validate_csr_subject(self): - return self.csr.subject == self.cert.subject - - def _validate_csr_extensions(self): - cert_exts = self.cert.extensions - csr_exts = self.csr.extensions - if len(cert_exts) != len(csr_exts): - return False - for cert_ext in cert_exts: - try: - csr_ext = csr_exts.get_extension_for_oid(cert_ext.oid) - if cert_ext != csr_ext: - return False - except cryptography.x509.ExtensionNotFound as dummy: - return False - return True - - def _validate_signature_algorithms(self): - if self.cert.signature_algorithm_oid._name not in self.signature_algorithms: - return self.cert.signature_algorithm_oid._name - - def _validate_subject(self): - expected_subject = Name([NameAttribute(oid=crypto_utils.cryptography_name_to_oid(sub[0]), value=to_text(sub[1])) - for sub in self.subject]) - cert_subject = self.cert.subject - if not compare_sets(expected_subject, cert_subject, self.subject_strict): - return expected_subject, cert_subject - - def _validate_issuer(self): - expected_issuer = Name([NameAttribute(oid=crypto_utils.cryptography_name_to_oid(iss[0]), value=to_text(iss[1])) - for iss in self.issuer]) - cert_issuer = self.cert.issuer - if not compare_sets(expected_issuer, cert_issuer, self.issuer_strict): - return self.issuer, cert_issuer - - def _validate_has_expired(self): - cert_not_after = self.cert.not_valid_after - cert_expired = cert_not_after < datetime.datetime.utcnow() - return cert_expired - - def _validate_version(self): - if self.cert.version == x509.Version.v1: - return 1 - if self.cert.version == x509.Version.v3: - return 3 - return "unknown" - - def _validate_key_usage(self): - try: - current_key_usage = self.cert.extensions.get_extension_for_class(x509.KeyUsage).value - test_key_usage = dict( - digital_signature=current_key_usage.digital_signature, - content_commitment=current_key_usage.content_commitment, - key_encipherment=current_key_usage.key_encipherment, - data_encipherment=current_key_usage.data_encipherment, - key_agreement=current_key_usage.key_agreement, - key_cert_sign=current_key_usage.key_cert_sign, - crl_sign=current_key_usage.crl_sign, - encipher_only=False, - decipher_only=False - ) - if test_key_usage['key_agreement']: - test_key_usage.update(dict( - encipher_only=current_key_usage.encipher_only, - decipher_only=current_key_usage.decipher_only - )) - - key_usages = crypto_utils.cryptography_parse_key_usage_params(self.key_usage) - if not compare_dicts(key_usages, test_key_usage, self.key_usage_strict): - return self.key_usage, [k for k, v in test_key_usage.items() if v is True] - - except cryptography.x509.ExtensionNotFound: - # This is only bad if the user specified a non-empty list - if self.key_usage: - return NO_EXTENSION - - def _validate_extended_key_usage(self): - try: - current_ext_keyusage = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value - usages = [crypto_utils.cryptography_name_to_oid(usage) for usage in self.extended_key_usage] - expected_ext_keyusage = x509.ExtendedKeyUsage(usages) - if not compare_sets(expected_ext_keyusage, current_ext_keyusage, self.extended_key_usage_strict): - return [eku.value for eku in expected_ext_keyusage], [eku.value for eku in current_ext_keyusage] - - except cryptography.x509.ExtensionNotFound: - # This is only bad if the user specified a non-empty list - if self.extended_key_usage: - return NO_EXTENSION - - def _validate_subject_alt_name(self): - try: - current_san = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value - expected_san = [crypto_utils.cryptography_get_name(san) for san in self.subject_alt_name] - if not compare_sets(expected_san, current_san, self.subject_alt_name_strict): - return self.subject_alt_name, current_san - except cryptography.x509.ExtensionNotFound: - # This is only bad if the user specified a non-empty list - if self.subject_alt_name: - return NO_EXTENSION - - def _validate_not_before(self): - return self.cert.not_valid_before - - def _validate_not_after(self): - return self.cert.not_valid_after - - def _validate_valid_at(self): - rt = crypto_utils.get_relative_time_option(self.valid_at, 'valid_at', backend=self.backend) - return self.cert.not_valid_before, rt, self.cert.not_valid_after - - def _validate_invalid_at(self): - rt = crypto_utils.get_relative_time_option(self.invalid_at, 'invalid_at', backend=self.backend) - return self.cert.not_valid_before, rt, self.cert.not_valid_after - - def _validate_valid_in(self): - valid_in_date = crypto_utils.get_relative_time_option(self.valid_in, "valid_in", backend=self.backend) - return self.cert.not_valid_before, valid_in_date, self.cert.not_valid_after - - -class AssertOnlyCertificate(AssertOnlyCertificateBase): - """validate the supplied certificate.""" - - def __init__(self, module): - super(AssertOnlyCertificate, self).__init__(module, 'pyopenssl') - - # Ensure inputs are properly sanitized before comparison. - for param in ['signature_algorithms', 'key_usage', 'extended_key_usage', - 'subject_alt_name', 'subject', 'issuer', 'not_before', - 'not_after', 'valid_at', 'invalid_at']: - attr = getattr(self, param) - if isinstance(attr, list) and attr: - if isinstance(attr[0], str): - setattr(self, param, [to_bytes(item) for item in attr]) - elif isinstance(attr[0], tuple): - setattr(self, param, [(to_bytes(item[0]), to_bytes(item[1])) for item in attr]) - elif isinstance(attr, tuple): - setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items())) - elif isinstance(attr, dict): - setattr(self, param, dict((to_bytes(k), to_bytes(v)) for (k, v) in attr.items())) - elif isinstance(attr, str): - setattr(self, param, to_bytes(attr)) - - def _validate_privatekey(self): - ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD) - ctx.use_privatekey(self.privatekey) - ctx.use_certificate(self.cert) - try: - ctx.check_privatekey() - return True - except OpenSSL.SSL.Error: - return False - - def _validate_csr_signature(self): - try: - self.csr.verify(self.cert.get_pubkey()) - except OpenSSL.crypto.Error: - return False - - def _validate_csr_subject(self): - if self.csr.get_subject() != self.cert.get_subject(): - return False - - def _validate_csr_extensions(self): - csr_extensions = self.csr.get_extensions() - cert_extension_count = self.cert.get_extension_count() - if len(csr_extensions) != cert_extension_count: - return False - for extension_number in range(0, cert_extension_count): - cert_extension = self.cert.get_extension(extension_number) - csr_extension = filter(lambda extension: extension.get_short_name() == cert_extension.get_short_name(), csr_extensions) - if cert_extension.get_data() != list(csr_extension)[0].get_data(): - return False - return True - - def _validate_signature_algorithms(self): - if self.cert.get_signature_algorithm() not in self.signature_algorithms: - return self.cert.get_signature_algorithm() - - def _validate_subject(self): - expected_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in self.subject] - cert_subject = self.cert.get_subject().get_components() - current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(sub[0]), sub[1]) for sub in cert_subject] - if not compare_sets(expected_subject, current_subject, self.subject_strict): - return expected_subject, current_subject - - def _validate_issuer(self): - expected_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in self.issuer] - cert_issuer = self.cert.get_issuer().get_components() - current_issuer = [(OpenSSL._util.lib.OBJ_txt2nid(iss[0]), iss[1]) for iss in cert_issuer] - if not compare_sets(expected_issuer, current_issuer, self.issuer_strict): - return self.issuer, cert_issuer - - def _validate_has_expired(self): - # The following 3 lines are the same as the current PyOpenSSL code for cert.has_expired(). - # Older version of PyOpenSSL have a buggy implementation, - # to avoid issues with those we added the code from a more recent release here. - - time_string = to_native(self.cert.get_notAfter()) - not_after = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ") - cert_expired = not_after < datetime.datetime.utcnow() - return cert_expired - - def _validate_version(self): - # Version numbers in certs are off by one: - # v1: 0, v2: 1, v3: 2 ... - return self.cert.get_version() + 1 - - def _validate_key_usage(self): - found = False - for extension_idx in range(0, self.cert.get_extension_count()): - extension = self.cert.get_extension(extension_idx) - if extension.get_short_name() == b'keyUsage': - found = True - expected_extension = crypto.X509Extension(b"keyUsage", False, b', '.join(self.key_usage)) - key_usage = [usage.strip() for usage in to_text(expected_extension, errors='surrogate_or_strict').split(',')] - current_ku = [usage.strip() for usage in to_text(extension, errors='surrogate_or_strict').split(',')] - if not compare_sets(key_usage, current_ku, self.key_usage_strict): - return self.key_usage, str(extension).split(', ') - if not found: - # This is only bad if the user specified a non-empty list - if self.key_usage: - return NO_EXTENSION - - def _validate_extended_key_usage(self): - found = False - for extension_idx in range(0, self.cert.get_extension_count()): - extension = self.cert.get_extension(extension_idx) - if extension.get_short_name() == b'extendedKeyUsage': - found = True - extKeyUsage = [OpenSSL._util.lib.OBJ_txt2nid(keyUsage) for keyUsage in self.extended_key_usage] - current_xku = [OpenSSL._util.lib.OBJ_txt2nid(usage.strip()) for usage in - to_bytes(extension, errors='surrogate_or_strict').split(b',')] - if not compare_sets(extKeyUsage, current_xku, self.extended_key_usage_strict): - return self.extended_key_usage, str(extension).split(', ') - if not found: - # This is only bad if the user specified a non-empty list - if self.extended_key_usage: - return NO_EXTENSION - - def _normalize_san(self, san): - # Apparently OpenSSL returns 'IP address' not 'IP' as specifier when converting the subjectAltName to string - # although it won't accept this specifier when generating the CSR. (https://github.com/openssl/openssl/issues/4004) - if san.startswith('IP Address:'): - san = 'IP:' + san[len('IP Address:'):] - if san.startswith('IP:'): - ip = compat_ipaddress.ip_address(san[3:]) - san = 'IP:{0}'.format(ip.compressed) - return san - - def _validate_subject_alt_name(self): - found = False - for extension_idx in range(0, self.cert.get_extension_count()): - extension = self.cert.get_extension(extension_idx) - if extension.get_short_name() == b'subjectAltName': - found = True - l_altnames = [self._normalize_san(altname.strip()) for altname in - to_text(extension, errors='surrogate_or_strict').split(', ')] - sans = [self._normalize_san(to_text(san, errors='surrogate_or_strict')) for san in self.subject_alt_name] - if not compare_sets(sans, l_altnames, self.subject_alt_name_strict): - return self.subject_alt_name, l_altnames - if not found: - # This is only bad if the user specified a non-empty list - if self.subject_alt_name: - return NO_EXTENSION - - def _validate_not_before(self): - return self.cert.get_notBefore() - - def _validate_not_after(self): - return self.cert.get_notAfter() - - def _validate_valid_at(self): - rt = crypto_utils.get_relative_time_option(self.valid_at, "valid_at", backend=self.backend) - rt = to_bytes(rt, errors='surrogate_or_strict') - return self.cert.get_notBefore(), rt, self.cert.get_notAfter() - - def _validate_invalid_at(self): - rt = crypto_utils.get_relative_time_option(self.invalid_at, "invalid_at", backend=self.backend) - rt = to_bytes(rt, errors='surrogate_or_strict') - return self.cert.get_notBefore(), rt, self.cert.get_notAfter() - - def _validate_valid_in(self): - valid_in_asn1 = crypto_utils.get_relative_time_option(self.valid_in, "valid_in", backend=self.backend) - valid_in_date = to_bytes(valid_in_asn1, errors='surrogate_or_strict') - return self.cert.get_notBefore(), valid_in_date, self.cert.get_notAfter() - - -class EntrustCertificate(Certificate): - """Retrieve a certificate using Entrust (ECS).""" - - def __init__(self, module, backend): - super(EntrustCertificate, self).__init__(module, backend) - self.trackingId = None - self.notAfter = crypto_utils.get_relative_time_option(module.params['entrust_not_after'], 'entrust_not_after', backend=self.backend) - - if self.csr_content is None or not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file {0} does not exist'.format(self.csr_path) - ) - - self.csr = crypto_utils.load_certificate_request( - path=self.csr_path, - content=self.csr_content, - backend=self.backend, - ) - - # ECS API defaults to using the validated organization tied to the account. - # We want to always force behavior of trying to use the organization provided in the CSR. - # To that end we need to parse out the organization from the CSR. - self.csr_org = None - if self.backend == 'pyopenssl': - csr_subject = self.csr.get_subject() - csr_subject_components = csr_subject.get_components() - for k, v in csr_subject_components: - if k.upper() == 'O': - # Entrust does not support multiple validated organizations in a single certificate - if self.csr_org is not None: - module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations found in " - "Subject DN: '{0}'. ".format(csr_subject))) - else: - self.csr_org = v - elif self.backend == 'cryptography': - csr_subject_orgs = self.csr.subject.get_attributes_for_oid(NameOID.ORGANIZATION_NAME) - if len(csr_subject_orgs) == 1: - self.csr_org = csr_subject_orgs[0].value - elif len(csr_subject_orgs) > 1: - module.fail_json(msg=("Entrust provider does not currently support multiple validated organizations. Multiple organizations found in " - "Subject DN: '{0}'. ".format(self.csr.subject))) - # If no organization in the CSR, explicitly tell ECS that it should be blank in issued cert, not defaulted to - # organization tied to the account. - if self.csr_org is None: - self.csr_org = '' - - try: - self.ecs_client = ECSClient( - entrust_api_user=module.params.get('entrust_api_user'), - entrust_api_key=module.params.get('entrust_api_key'), - entrust_api_cert=module.params.get('entrust_api_client_cert_path'), - entrust_api_cert_key=module.params.get('entrust_api_client_cert_key_path'), - entrust_api_specification_path=module.params.get('entrust_api_specification_path') - ) - except SessionConfigurationException as e: - module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e.message))) - - def generate(self, module): - - if not self.check(module, perms_required=False) or self.force: - # Read the CSR that was generated for us - body = {} - if self.csr_content is not None: - body['csr'] = self.csr_content - else: - with open(self.csr_path, 'r') as csr_file: - body['csr'] = csr_file.read() - - body['certType'] = module.params['entrust_cert_type'] - - # Handle expiration (30 days if not specified) - expiry = self.notAfter - if not expiry: - gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime())) - expiry = gmt_now + datetime.timedelta(days=365) - - expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z") - body['certExpiryDate'] = expiry_iso3339 - body['org'] = self.csr_org - body['tracking'] = { - 'requesterName': module.params['entrust_requester_name'], - 'requesterEmail': module.params['entrust_requester_email'], - 'requesterPhone': module.params['entrust_requester_phone'], - } - - try: - result = self.ecs_client.NewCertRequest(Body=body) - self.trackingId = result.get('trackingId') - except RestOperationException as e: - module.fail_json(msg='Failed to request new certificate from Entrust Certificate Services (ECS): {0}'.format(to_native(e.message))) - - if self.backup: - self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, to_bytes(result.get('endEntityCert'))) - self.cert = crypto_utils.load_certificate(self.path, backend=self.backend) - self.changed = True - - def check(self, module, perms_required=True): - """Ensure the resource is in its desired state.""" - - parent_check = super(EntrustCertificate, self).check(module, perms_required) - - try: - cert_details = self._get_cert_details() - except RestOperationException as e: - module.fail_json(msg='Failed to get status of existing certificate from Entrust Certificate Services (ECS): {0}.'.format(to_native(e.message))) - - # Always issue a new certificate if the certificate is expired, suspended or revoked - status = cert_details.get('status', False) - if status == 'EXPIRED' or status == 'SUSPENDED' or status == 'REVOKED': - return False - - # If the requested cert type was specified and it is for a different certificate type than the initial certificate, a new one is needed - if module.params['entrust_cert_type'] and cert_details.get('certType') and module.params['entrust_cert_type'] != cert_details.get('certType'): - return False - - return parent_check - - def _get_cert_details(self): - cert_details = {} - if self.cert: - serial_number = None - expiry = None - if self.backend == 'pyopenssl': - serial_number = "{0:X}".format(self.cert.get_serial_number()) - time_string = to_native(self.cert.get_notAfter()) - expiry = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ") - elif self.backend == 'cryptography': - serial_number = "{0:X}".format(self.cert.serial_number) - expiry = self.cert.not_valid_after - - # get some information about the expiry of this certificate - expiry_iso3339 = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z") - cert_details['expiresAfter'] = expiry_iso3339 - - # If a trackingId is not already defined (from the result of a generate) - # use the serial number to identify the tracking Id - if self.trackingId is None and serial_number is not None: - cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {}) - - # Finding 0 or more than 1 result is a very unlikely use case, it simply means we cannot perform additional checks - # on the 'state' as returned by Entrust Certificate Services (ECS). The general certificate validity is - # still checked as it is in the rest of the module. - if len(cert_results) == 1: - self.trackingId = cert_results[0].get('trackingId') - - if self.trackingId is not None: - cert_details.update(self.ecs_client.GetCertificate(trackingId=self.trackingId)) - - return cert_details - - def dump(self, check_mode=False): - - result = { - 'changed': self.changed, - 'filename': self.path, - 'privatekey': self.privatekey_path, - 'csr': self.csr_path, - } - - if self.backup_file: - result['backup_file'] = self.backup_file - if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - result['certificate'] = content.decode('utf-8') if content else None - - result.update(self._get_cert_details()) - - return result - - -class AcmeCertificate(Certificate): - """Retrieve a certificate using the ACME protocol.""" - - # Since there's no real use of the backend, - # other than the 'self.check' function, we just pass the backend to the constructor - - def __init__(self, module, backend): - super(AcmeCertificate, self).__init__(module, backend) - self.accountkey_path = module.params['acme_accountkey_path'] - self.challenge_path = module.params['acme_challenge_path'] - self.use_chain = module.params['acme_chain'] - self.acme_directory = module.params['acme_directory'] - - def generate(self, module): - - if self.csr_content is None and not os.path.exists(self.csr_path): - raise CertificateError( - 'The certificate signing request file %s does not exist' % self.csr_path - ) - - if not os.path.exists(self.accountkey_path): - raise CertificateError( - 'The account key %s does not exist' % self.accountkey_path - ) - - if not os.path.exists(self.challenge_path): - raise CertificateError( - 'The challenge path %s does not exist' % self.challenge_path - ) - - if not self.check(module, perms_required=False) or self.force: - acme_tiny_path = self.module.get_bin_path('acme-tiny', required=True) - command = [acme_tiny_path] - if self.use_chain: - command.append('--chain') - command.extend(['--account-key', self.accountkey_path]) - if self.csr_content is not None: - # We need to temporarily write the CSR to disk - fd, tmpsrc = tempfile.mkstemp() - module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit - f = os.fdopen(fd, 'wb') - try: - f.write(self.csr_content) - except Exception as err: - try: - f.close() - except Exception as dummy: - pass - module.fail_json( - msg="failed to create temporary CSR file: %s" % to_native(err), - exception=traceback.format_exc() - ) - f.close() - command.extend(['--csr', tmpsrc]) - else: - command.extend(['--csr', self.csr_path]) - command.extend(['--acme-dir', self.challenge_path]) - command.extend(['--directory-url', self.acme_directory]) - - try: - crt = module.run_command(command, check_rc=True)[1] - if self.backup: - self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, to_bytes(crt)) - self.changed = True - except OSError as exc: - raise CertificateError(exc) - - file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, False): - self.changed = True - - def dump(self, check_mode=False): - - result = { - 'changed': self.changed, - 'filename': self.path, - 'privatekey': self.privatekey_path, - 'accountkey': self.accountkey_path, - 'csr': self.csr_path, - } - if self.backup_file: - result['backup_file'] = self.backup_file - if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - result['certificate'] = content.decode('utf-8') if content else None - - return result - - -def main(): - module = AnsibleModule( - argument_spec=dict( - state=dict(type='str', default='present', choices=['present', 'absent']), - path=dict(type='path', required=True), - provider=dict(type='str', choices=['acme', 'assertonly', 'entrust', 'ownca', 'selfsigned']), - force=dict(type='bool', default=False,), - csr_path=dict(type='path'), - csr_content=dict(type='str'), - backup=dict(type='bool', default=False), - select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), - return_content=dict(type='bool', default=False), - - # General properties of a certificate - privatekey_path=dict(type='path'), - privatekey_content=dict(type='str'), - privatekey_passphrase=dict(type='str', no_log=True), - - # provider: assertonly - signature_algorithms=dict(type='list', elements='str', removed_in_version='2.13'), - subject=dict(type='dict', removed_in_version='2.13'), - subject_strict=dict(type='bool', default=False, removed_in_version='2.13'), - issuer=dict(type='dict', removed_in_version='2.13'), - issuer_strict=dict(type='bool', default=False, removed_in_version='2.13'), - has_expired=dict(type='bool', default=False, removed_in_version='2.13'), - version=dict(type='int', removed_in_version='2.13'), - key_usage=dict(type='list', elements='str', aliases=['keyUsage'], removed_in_version='2.13'), - key_usage_strict=dict(type='bool', default=False, aliases=['keyUsage_strict'], removed_in_version='2.13'), - extended_key_usage=dict(type='list', elements='str', aliases=['extendedKeyUsage'], removed_in_version='2.13'), - extended_key_usage_strict=dict(type='bool', default=False, aliases=['extendedKeyUsage_strict'], removed_in_version='2.13'), - subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName'], removed_in_version='2.13'), - subject_alt_name_strict=dict(type='bool', default=False, aliases=['subjectAltName_strict'], removed_in_version='2.13'), - not_before=dict(type='str', aliases=['notBefore'], removed_in_version='2.13'), - not_after=dict(type='str', aliases=['notAfter'], removed_in_version='2.13'), - valid_at=dict(type='str', removed_in_version='2.13'), - invalid_at=dict(type='str', removed_in_version='2.13'), - valid_in=dict(type='str', removed_in_version='2.13'), - - # provider: selfsigned - selfsigned_version=dict(type='int', default=3), - selfsigned_digest=dict(type='str', default='sha256'), - selfsigned_not_before=dict(type='str', default='+0s', aliases=['selfsigned_notBefore']), - selfsigned_not_after=dict(type='str', default='+3650d', aliases=['selfsigned_notAfter']), - selfsigned_create_subject_key_identifier=dict( - type='str', - default='create_if_not_provided', - choices=['create_if_not_provided', 'always_create', 'never_create'] - ), - - # provider: ownca - ownca_path=dict(type='path'), - ownca_content=dict(type='str'), - ownca_privatekey_path=dict(type='path'), - ownca_privatekey_content=dict(type='str'), - ownca_privatekey_passphrase=dict(type='str', no_log=True), - ownca_digest=dict(type='str', default='sha256'), - ownca_version=dict(type='int', default=3), - ownca_not_before=dict(type='str', default='+0s'), - ownca_not_after=dict(type='str', default='+3650d'), - ownca_create_subject_key_identifier=dict( - type='str', - default='create_if_not_provided', - choices=['create_if_not_provided', 'always_create', 'never_create'] - ), - ownca_create_authority_key_identifier=dict(type='bool', default=True), - - # provider: acme - acme_accountkey_path=dict(type='path'), - acme_challenge_path=dict(type='path'), - acme_chain=dict(type='bool', default=False), - acme_directory=dict(type='str', default="https://acme-v02.api.letsencrypt.org/directory"), - - # provider: entrust - entrust_cert_type=dict(type='str', default='STANDARD_SSL', - choices=['STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', - 'PRIVATE_SSL', 'PD_SSL', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT']), - entrust_requester_email=dict(type='str'), - entrust_requester_name=dict(type='str'), - entrust_requester_phone=dict(type='str'), - entrust_api_user=dict(type='str'), - entrust_api_key=dict(type='str', no_log=True), - entrust_api_client_cert_path=dict(type='path'), - entrust_api_client_cert_key_path=dict(type='path', no_log=True), - entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'), - entrust_not_after=dict(type='str', default='+365d'), - ), - supports_check_mode=True, - add_file_common_args=True, - required_if=[ - ['state', 'present', ['provider']], - ['provider', 'entrust', ['entrust_requester_email', 'entrust_requester_name', 'entrust_requester_phone', - 'entrust_api_user', 'entrust_api_key', 'entrust_api_client_cert_path', - 'entrust_api_client_cert_key_path']], - ], - mutually_exclusive=[ - ['csr_path', 'csr_content'], - ['privatekey_path', 'privatekey_content'], - ['ownca_path', 'ownca_content'], - ['ownca_privatekey_path', 'ownca_privatekey_content'], - ], - ) - - try: - if module.params['state'] == 'absent': - certificate = CertificateAbsent(module) - - else: - if module.params['provider'] != 'assertonly' and module.params['csr_path'] is None and module.params['csr_content'] is None: - module.fail_json(msg='csr_path or csr_content is required when provider is not assertonly') - - base_dir = os.path.dirname(module.params['path']) or '.' - if not os.path.isdir(base_dir): - module.fail_json( - name=base_dir, - msg='The directory %s does not exist or the file is not a directory' % base_dir - ) - - provider = module.params['provider'] - if provider == 'assertonly': - module.deprecate("The 'assertonly' provider is deprecated; please see the examples of " - "the 'openssl_certificate' module on how to replace it with other modules", - version='2.13') - elif provider == 'selfsigned': - if module.params['privatekey_path'] is None and module.params['privatekey_content'] is None: - module.fail_json(msg='One of privatekey_path and privatekey_content must be specified for the selfsigned provider.') - elif provider == 'acme': - if module.params['acme_accountkey_path'] is None: - module.fail_json(msg='The acme_accountkey_path option must be specified for the acme provider.') - if module.params['acme_challenge_path'] is None: - module.fail_json(msg='The acme_challenge_path option must be specified for the acme provider.') - elif provider == 'ownca': - if module.params['ownca_path'] is None and module.params['ownca_content'] is None: - module.fail_json(msg='One of ownca_path and ownca_content must be specified for the ownca provider.') - if module.params['ownca_privatekey_path'] is None and module.params['ownca_privatekey_content'] is None: - module.fail_json(msg='One of ownca_privatekey_path and ownca_privatekey_content must be specified for the ownca provider.') - - backend = module.params['select_crypto_backend'] - if backend == 'auto': - # Detect what backend we can use - can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) - can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) - - # If cryptography is available we'll use it - if can_use_cryptography: - backend = 'cryptography' - elif can_use_pyopenssl: - backend = 'pyopenssl' - - if module.params['selfsigned_version'] == 2 or module.params['ownca_version'] == 2: - module.warn('crypto backend forced to pyopenssl. The cryptography library does not support v2 certificates') - backend = 'pyopenssl' - - # Fail if no backend has been found - if backend == 'auto': - module.fail_json(msg=("Can't detect any of the required Python libraries " - "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( - MINIMAL_CRYPTOGRAPHY_VERSION, - MINIMAL_PYOPENSSL_VERSION)) - - if backend == 'pyopenssl': - if not PYOPENSSL_FOUND: - module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), - exception=PYOPENSSL_IMP_ERR) - if module.params['provider'] in ['selfsigned', 'ownca', 'assertonly']: - try: - getattr(crypto.X509Req, 'get_extensions') - except AttributeError: - module.fail_json(msg='You need to have PyOpenSSL>=0.15') - - module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') - if provider == 'selfsigned': - certificate = SelfSignedCertificate(module) - elif provider == 'acme': - certificate = AcmeCertificate(module, 'pyopenssl') - elif provider == 'ownca': - certificate = OwnCACertificate(module) - elif provider == 'entrust': - certificate = EntrustCertificate(module, 'pyopenssl') - else: - certificate = AssertOnlyCertificate(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) - if module.params['selfsigned_version'] == 2 or module.params['ownca_version'] == 2: - module.fail_json(msg='The cryptography backend does not support v2 certificates, ' - 'use select_crypto_backend=pyopenssl for v2 certificates') - if provider == 'selfsigned': - certificate = SelfSignedCertificateCryptography(module) - elif provider == 'acme': - certificate = AcmeCertificate(module, 'cryptography') - elif provider == 'ownca': - certificate = OwnCACertificateCryptography(module) - elif provider == 'entrust': - certificate = EntrustCertificate(module, 'cryptography') - else: - certificate = AssertOnlyCertificateCryptography(module) - - if module.params['state'] == 'present': - if module.check_mode: - result = certificate.dump(check_mode=True) - result['changed'] = module.params['force'] or not certificate.check(module) - module.exit_json(**result) - - certificate.generate(module) - else: - if module.check_mode: - result = certificate.dump(check_mode=True) - result['changed'] = os.path.exists(module.params['path']) - module.exit_json(**result) - - certificate.remove(module) - - result = certificate.dump() - module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: - module.fail_json(msg=to_native(exc)) - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/crypto/openssl_certificate_info.py b/lib/ansible/modules/crypto/openssl_certificate_info.py deleted file mode 100644 index 2d7459ae9d..0000000000 --- a/lib/ansible/modules/crypto/openssl_certificate_info.py +++ /dev/null @@ -1,863 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org> -# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: openssl_certificate_info -version_added: '2.8' -short_description: Provide information of OpenSSL X.509 certificates -description: - - This module allows one to query information on OpenSSL certificates. - - It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the - cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements) - cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with - C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 - and will be removed in Ansible 2.13. -requirements: - - PyOpenSSL >= 0.15 or cryptography >= 1.6 -author: - - Felix Fontein (@felixfontein) - - Yanis Guenane (@Spredzy) - - Markus Teufelberger (@MarkusTeufelberger) -options: - path: - description: - - Remote absolute path where the certificate file is loaded from. - - Either I(path) or I(content) must be specified, but not both. - type: path - content: - description: - - Content of the X.509 certificate in PEM format. - - Either I(path) or I(content) must be specified, but not both. - type: str - version_added: "2.10" - valid_at: - description: - - A dict of names mapping to time specifications. Every time specified here - will be checked whether the certificate is valid at this point. See the - C(valid_at) return value for informations on the result. - - Time can be specified either as relative time or as absolute timestamp. - - Time will always be interpreted as UTC. - - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer - + C([w | d | h | m | s]) (e.g. C(+32w1d2h), and ASN.1 TIME (i.e. pattern C(YYYYMMDDHHMMSSZ)). - Note that all timestamps will be treated as being in UTC. - type: dict - 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(pyopenssl). - - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. - - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. - - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. - From that point on, only the C(cryptography) backend will be available. - type: str - default: auto - choices: [ auto, cryptography, pyopenssl ] - -notes: - - All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern. - They are all in UTC. -seealso: -- module: openssl_certificate -''' - -EXAMPLES = r''' -- name: Generate a Self Signed OpenSSL certificate - openssl_certificate: - path: /etc/ssl/crt/ansible.com.crt - privatekey_path: /etc/ssl/private/ansible.com.pem - csr_path: /etc/ssl/csr/ansible.com.csr - provider: selfsigned - - -# Get information on the certificate - -- name: Get information on generated certificate - openssl_certificate_info: - path: /etc/ssl/crt/ansible.com.crt - register: result - -- name: Dump information - debug: - var: result - - -# Check whether the certificate is valid or not valid at certain times, fail -# if this is not the case. The first task (openssl_certificate_info) collects -# the information, and the second task (assert) validates the result and -# makes the playbook fail in case something is not as expected. - -- name: Test whether that certificate is valid tomorrow and/or in three weeks - openssl_certificate_info: - path: /etc/ssl/crt/ansible.com.crt - valid_at: - point_1: "+1d" - point_2: "+3w" - register: result - -- name: Validate that certificate is valid tomorrow, but not in three weeks - assert: - that: - - result.valid_at.point_1 # valid in one day - - not result.valid_at.point_2 # not valid in three weeks -''' - -RETURN = r''' -expired: - description: Whether the certificate is expired (i.e. C(notAfter) is in the past) - returned: success - type: bool -basic_constraints: - description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present. - returned: success - type: list - elements: str - sample: "[CA:TRUE, pathlen:1]" -basic_constraints_critical: - description: Whether the C(basic_constraints) extension is critical. - returned: success - type: bool -extended_key_usage: - description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present. - returned: success - type: list - elements: str - sample: "[Biometric Info, DVCS, Time Stamping]" -extended_key_usage_critical: - description: Whether the C(extended_key_usage) extension is critical. - returned: success - type: bool -extensions_by_oid: - description: Returns a dictionary for every extension OID - returned: success - type: dict - contains: - critical: - description: Whether the extension is critical. - returned: success - type: bool - value: - description: The Base64 encoded value (in DER format) of the extension - returned: success - type: str - sample: "MAMCAQU=" - sample: '{"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}' -key_usage: - description: Entries in the C(key_usage) extension, or C(none) if extension is not present. - returned: success - type: str - sample: "[Key Agreement, Data Encipherment]" -key_usage_critical: - description: Whether the C(key_usage) extension is critical. - returned: success - type: bool -subject_alt_name: - description: Entries in the C(subject_alt_name) extension, or C(none) if extension is not present. - returned: success - type: list - elements: str - sample: "[DNS:www.ansible.com, IP:1.2.3.4]" -subject_alt_name_critical: - description: Whether the C(subject_alt_name) extension is critical. - returned: success - type: bool -ocsp_must_staple: - description: C(yes) if the OCSP Must Staple extension is present, C(none) otherwise. - returned: success - type: bool -ocsp_must_staple_critical: - description: Whether the C(ocsp_must_staple) extension is critical. - returned: success - type: bool -issuer: - description: - - The certificate's issuer. - - Note that for repeated values, only the last one will be returned. - returned: success - type: dict - sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}' -issuer_ordered: - description: The certificate's issuer as an ordered list of tuples. - returned: success - type: list - elements: list - sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]' - version_added: "2.9" -subject: - description: - - The certificate's subject as a dictionary. - - Note that for repeated values, only the last one will be returned. - returned: success - type: dict - sample: '{"commonName": "www.example.com", "emailAddress": "test@example.com"}' -subject_ordered: - description: The certificate's subject as an ordered list of tuples. - returned: success - type: list - elements: list - sample: '[["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]' - version_added: "2.9" -not_after: - description: C(notAfter) date as ASN.1 TIME - returned: success - type: str - sample: 20190413202428Z -not_before: - description: C(notBefore) date as ASN.1 TIME - returned: success - type: str - sample: 20190331202428Z -public_key: - description: Certificate's public key in PEM format - returned: success - type: str - sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..." -public_key_fingerprints: - description: - - Fingerprints of certificate'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..." -signature_algorithm: - description: The signature algorithm used to sign the certificate. - returned: success - type: str - sample: sha256WithRSAEncryption -serial_number: - description: The certificate's serial number. - returned: success - type: int - sample: 1234 -version: - description: The certificate version. - returned: success - type: int - sample: 3 -valid_at: - description: For every time stamp provided in the I(valid_at) option, a - boolean whether the certificate is valid at that point in time - or not. - returned: success - type: dict -subject_key_identifier: - description: - - The certificate's subject key identifier. - - The identifier is returned in hexadecimal, with C(:) used to separate bytes. - - Is C(none) if the C(SubjectKeyIdentifier) extension is not present. - returned: success and if the pyOpenSSL backend is I(not) used - type: str - sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' - version_added: "2.9" -authority_key_identifier: - description: - - The certificate's authority key identifier. - - The identifier is returned in hexadecimal, with C(:) used to separate bytes. - - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. - returned: success and if the pyOpenSSL backend is I(not) used - type: str - sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' - version_added: "2.9" -authority_cert_issuer: - description: - - The certificate's authority cert issuer as a list of general names. - - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. - returned: success and if the pyOpenSSL backend is I(not) used - type: list - elements: str - sample: "[DNS:www.ansible.com, IP:1.2.3.4]" - version_added: "2.9" -authority_cert_serial_number: - description: - - The certificate's authority cert serial number. - - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. - returned: success and if the pyOpenSSL backend is I(not) used - type: int - sample: '12345' - version_added: "2.9" -ocsp_uri: - description: The OCSP responder URI, if included in the certificate. Will be - C(none) if no OCSP responder URI is included. - returned: success - type: str - version_added: "2.9" -''' - - -import abc -import binascii -import datetime -import os -import re -import traceback -from distutils.version import LooseVersion - -from ansible.module_utils import crypto as crypto_utils -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils.six import string_types -from ansible.module_utils._text import to_native, to_text, to_bytes -from ansible.module_utils.compat import ipaddress as compat_ipaddress - -MINIMAL_CRYPTOGRAPHY_VERSION = '1.6' -MINIMAL_PYOPENSSL_VERSION = '0.15' - -PYOPENSSL_IMP_ERR = None -try: - import OpenSSL - from OpenSSL import crypto - PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) - if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000: - # OpenSSL 1.1.0 or newer - OPENSSL_MUST_STAPLE_NAME = b"tlsfeature" - OPENSSL_MUST_STAPLE_VALUE = b"status_request" - else: - # OpenSSL 1.0.x or older - OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24" - OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05" -except ImportError: - PYOPENSSL_IMP_ERR = traceback.format_exc() - PYOPENSSL_FOUND = False -else: - PYOPENSSL_FOUND = True - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - from cryptography import x509 - from cryptography.hazmat.primitives import serialization - CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - - -TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" - - -class CertificateInfo(crypto_utils.OpenSSLObject): - def __init__(self, module, backend): - super(CertificateInfo, self).__init__( - module.params['path'] or '', - 'present', - False, - module.check_mode, - ) - self.backend = backend - self.module = module - self.content = module.params['content'] - if self.content is not None: - self.content = self.content.encode('utf-8') - - self.valid_at = module.params['valid_at'] - if self.valid_at: - for k, v in self.valid_at.items(): - if not isinstance(v, string_types): - self.module.fail_json( - msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v)) - ) - self.valid_at[k] = crypto_utils.get_relative_time_option(v, 'valid_at.{0}'.format(k)) - - def generate(self): - # Empty method because crypto_utils.OpenSSLObject wants this - pass - - def dump(self): - # Empty method because crypto_utils.OpenSSLObject wants this - pass - - @abc.abstractmethod - def _get_signature_algorithm(self): - pass - - @abc.abstractmethod - def _get_subject_ordered(self): - pass - - @abc.abstractmethod - def _get_issuer_ordered(self): - pass - - @abc.abstractmethod - def _get_version(self): - pass - - @abc.abstractmethod - def _get_key_usage(self): - pass - - @abc.abstractmethod - def _get_extended_key_usage(self): - pass - - @abc.abstractmethod - def _get_basic_constraints(self): - pass - - @abc.abstractmethod - def _get_ocsp_must_staple(self): - pass - - @abc.abstractmethod - def _get_subject_alt_name(self): - pass - - @abc.abstractmethod - def _get_not_before(self): - pass - - @abc.abstractmethod - def _get_not_after(self): - pass - - @abc.abstractmethod - def _get_public_key(self, binary): - pass - - @abc.abstractmethod - def _get_subject_key_identifier(self): - pass - - @abc.abstractmethod - def _get_authority_key_identifier(self): - pass - - @abc.abstractmethod - def _get_serial_number(self): - pass - - @abc.abstractmethod - def _get_all_extensions(self): - pass - - @abc.abstractmethod - def _get_ocsp_uri(self): - pass - - def get_info(self): - result = dict() - self.cert = crypto_utils.load_certificate(self.path, content=self.content, backend=self.backend) - - result['signature_algorithm'] = self._get_signature_algorithm() - subject = self._get_subject_ordered() - issuer = self._get_issuer_ordered() - result['subject'] = dict() - for k, v in subject: - result['subject'][k] = v - result['subject_ordered'] = subject - result['issuer'] = dict() - for k, v in issuer: - result['issuer'][k] = v - result['issuer_ordered'] = issuer - result['version'] = self._get_version() - result['key_usage'], result['key_usage_critical'] = self._get_key_usage() - result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage() - result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints() - result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple() - result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name() - - not_before = self._get_not_before() - not_after = self._get_not_after() - result['not_before'] = not_before.strftime(TIMESTAMP_FORMAT) - result['not_after'] = not_after.strftime(TIMESTAMP_FORMAT) - result['expired'] = not_after < datetime.datetime.utcnow() - - result['valid_at'] = dict() - if self.valid_at: - for k, v in self.valid_at.items(): - result['valid_at'][k] = not_before <= v <= not_after - - result['public_key'] = self._get_public_key(binary=False) - pk = self._get_public_key(binary=True) - result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict() - - if self.backend != 'pyopenssl': - ski = self._get_subject_key_identifier() - if ski is not None: - ski = to_native(binascii.hexlify(ski)) - ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)]) - result['subject_key_identifier'] = ski - - aki, aci, acsn = self._get_authority_key_identifier() - if aki is not None: - aki = to_native(binascii.hexlify(aki)) - aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)]) - result['authority_key_identifier'] = aki - result['authority_cert_issuer'] = aci - result['authority_cert_serial_number'] = acsn - - result['serial_number'] = self._get_serial_number() - result['extensions_by_oid'] = self._get_all_extensions() - result['ocsp_uri'] = self._get_ocsp_uri() - - return result - - -class CertificateInfoCryptography(CertificateInfo): - """Validate the supplied cert, using the cryptography backend""" - def __init__(self, module): - super(CertificateInfoCryptography, self).__init__(module, 'cryptography') - - def _get_signature_algorithm(self): - return crypto_utils.cryptography_oid_to_name(self.cert.signature_algorithm_oid) - - def _get_subject_ordered(self): - result = [] - for attribute in self.cert.subject: - result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) - return result - - def _get_issuer_ordered(self): - result = [] - for attribute in self.cert.issuer: - result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) - return result - - def _get_version(self): - if self.cert.version == x509.Version.v1: - return 1 - if self.cert.version == x509.Version.v3: - return 3 - return "unknown" - - def _get_key_usage(self): - try: - current_key_ext = self.cert.extensions.get_extension_for_class(x509.KeyUsage) - current_key_usage = current_key_ext.value - key_usage = dict( - digital_signature=current_key_usage.digital_signature, - content_commitment=current_key_usage.content_commitment, - key_encipherment=current_key_usage.key_encipherment, - data_encipherment=current_key_usage.data_encipherment, - key_agreement=current_key_usage.key_agreement, - key_cert_sign=current_key_usage.key_cert_sign, - crl_sign=current_key_usage.crl_sign, - encipher_only=False, - decipher_only=False, - ) - if key_usage['key_agreement']: - key_usage.update(dict( - encipher_only=current_key_usage.encipher_only, - decipher_only=current_key_usage.decipher_only - )) - - key_usage_names = dict( - digital_signature='Digital Signature', - content_commitment='Non Repudiation', - key_encipherment='Key Encipherment', - data_encipherment='Data Encipherment', - key_agreement='Key Agreement', - key_cert_sign='Certificate Sign', - crl_sign='CRL Sign', - encipher_only='Encipher Only', - decipher_only='Decipher Only', - ) - return sorted([ - key_usage_names[name] for name, value in key_usage.items() if value - ]), current_key_ext.critical - except cryptography.x509.ExtensionNotFound: - return None, False - - def _get_extended_key_usage(self): - try: - ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage) - return sorted([ - crypto_utils.cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value - ]), ext_keyusage_ext.critical - except cryptography.x509.ExtensionNotFound: - return None, False - - def _get_basic_constraints(self): - try: - ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.BasicConstraints) - result = [] - result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE')) - if ext_keyusage_ext.value.path_length is not None: - result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length)) - return sorted(result), ext_keyusage_ext.critical - except cryptography.x509.ExtensionNotFound: - return None, False - - def _get_ocsp_must_staple(self): - try: - try: - # This only works with cryptography >= 2.1 - tlsfeature_ext = self.cert.extensions.get_extension_for_class(x509.TLSFeature) - value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value - except AttributeError as dummy: - # Fallback for cryptography < 2.1 - oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24") - tlsfeature_ext = self.cert.extensions.get_extension_for_oid(oid) - value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05" - return value, tlsfeature_ext.critical - except cryptography.x509.ExtensionNotFound: - return None, False - - def _get_subject_alt_name(self): - try: - san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName) - result = [crypto_utils.cryptography_decode_name(san) for san in san_ext.value] - return result, san_ext.critical - except cryptography.x509.ExtensionNotFound: - return None, False - - def _get_not_before(self): - return self.cert.not_valid_before - - def _get_not_after(self): - return self.cert.not_valid_after - - def _get_public_key(self, binary): - return self.cert.public_key().public_bytes( - serialization.Encoding.DER if binary else serialization.Encoding.PEM, - serialization.PublicFormat.SubjectPublicKeyInfo - ) - - def _get_subject_key_identifier(self): - try: - ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) - return ext.value.digest - except cryptography.x509.ExtensionNotFound: - return None - - def _get_authority_key_identifier(self): - try: - ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) - issuer = None - if ext.value.authority_cert_issuer is not None: - issuer = [crypto_utils.cryptography_decode_name(san) for san in ext.value.authority_cert_issuer] - return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number - except cryptography.x509.ExtensionNotFound: - return None, None, None - - def _get_serial_number(self): - return self.cert.serial_number - - def _get_all_extensions(self): - return crypto_utils.cryptography_get_extensions_from_cert(self.cert) - - def _get_ocsp_uri(self): - try: - ext = self.cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess) - for desc in ext.value: - if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP: - if isinstance(desc.access_location, x509.UniformResourceIdentifier): - return desc.access_location.value - except x509.ExtensionNotFound as dummy: - pass - return None - - -class CertificateInfoPyOpenSSL(CertificateInfo): - """validate the supplied certificate.""" - - def __init__(self, module): - super(CertificateInfoPyOpenSSL, self).__init__(module, 'pyopenssl') - - def _get_signature_algorithm(self): - return to_text(self.cert.get_signature_algorithm()) - - def __get_name(self, name): - result = [] - for sub in name.get_components(): - result.append([crypto_utils.pyopenssl_normalize_name(sub[0]), to_text(sub[1])]) - return result - - def _get_subject_ordered(self): - return self.__get_name(self.cert.get_subject()) - - def _get_issuer_ordered(self): - return self.__get_name(self.cert.get_issuer()) - - def _get_version(self): - # Version numbers in certs are off by one: - # v1: 0, v2: 1, v3: 2 ... - return self.cert.get_version() + 1 - - def _get_extension(self, short_name): - for extension_idx in range(0, self.cert.get_extension_count()): - extension = self.cert.get_extension(extension_idx) - if extension.get_short_name() == short_name: - result = [ - crypto_utils.pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',') - ] - return sorted(result), bool(extension.get_critical()) - return None, False - - def _get_key_usage(self): - return self._get_extension(b'keyUsage') - - def _get_extended_key_usage(self): - return self._get_extension(b'extendedKeyUsage') - - def _get_basic_constraints(self): - return self._get_extension(b'basicConstraints') - - def _get_ocsp_must_staple(self): - extensions = [self.cert.get_extension(i) for i in range(0, self.cert.get_extension_count())] - oms_ext = [ - ext for ext in extensions - if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE - ] - if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000: - # Older versions of libssl don't know about OCSP Must Staple - oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05']) - if oms_ext: - return True, bool(oms_ext[0].get_critical()) - else: - return None, False - - def _normalize_san(self, san): - if san.startswith('IP Address:'): - san = 'IP:' + san[len('IP Address:'):] - if san.startswith('IP:'): - ip = compat_ipaddress.ip_address(san[3:]) - san = 'IP:{0}'.format(ip.compressed) - return san - - def _get_subject_alt_name(self): - for extension_idx in range(0, self.cert.get_extension_count()): - extension = self.cert.get_extension(extension_idx) - if extension.get_short_name() == b'subjectAltName': - result = [self._normalize_san(altname.strip()) for altname in - to_text(extension, errors='surrogate_or_strict').split(', ')] - return result, bool(extension.get_critical()) - return None, False - - def _get_not_before(self): - time_string = to_native(self.cert.get_notBefore()) - return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ") - - def _get_not_after(self): - time_string = to_native(self.cert.get_notAfter()) - return datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ") - - def _get_public_key(self, binary): - try: - return crypto.dump_publickey( - crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM, - self.cert.get_pubkey() - ) - except AttributeError: - try: - # pyOpenSSL < 16.0: - bio = crypto._new_mem_buf() - if binary: - rc = crypto._lib.i2d_PUBKEY_bio(bio, self.cert.get_pubkey()._pkey) - else: - rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.cert.get_pubkey()._pkey) - if rc != 1: - crypto._raise_current_error() - return crypto._bio_to_string(bio) - except AttributeError: - self.module.warn('Your pyOpenSSL version does not support dumping public keys. ' - 'Please upgrade to version 16.0 or newer, or use the cryptography backend.') - - def _get_subject_key_identifier(self): - # Won't be implemented - return None - - def _get_authority_key_identifier(self): - # Won't be implemented - return None, None, None - - def _get_serial_number(self): - return self.cert.get_serial_number() - - def _get_all_extensions(self): - return crypto_utils.pyopenssl_get_extensions_from_cert(self.cert) - - def _get_ocsp_uri(self): - for i in range(self.cert.get_extension_count()): - ext = self.cert.get_extension(i) - if ext.get_short_name() == b'authorityInfoAccess': - v = str(ext) - m = re.search('^OCSP - URI:(.*)$', v, flags=re.MULTILINE) - if m: - return m.group(1) - return None - - -def main(): - module = AnsibleModule( - argument_spec=dict( - path=dict(type='path'), - content=dict(type='str'), - valid_at=dict(type='dict'), - select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), - ), - required_one_of=( - ['path', 'content'], - ), - mutually_exclusive=( - ['path', 'content'], - ), - supports_check_mode=True, - ) - - try: - if module.params['path'] is not None: - base_dir = os.path.dirname(module.params['path']) or '.' - if not os.path.isdir(base_dir): - module.fail_json( - name=base_dir, - msg='The directory %s does not exist or the file is not a directory' % base_dir - ) - - backend = module.params['select_crypto_backend'] - if backend == 'auto': - # Detect what backend we can use - can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) - can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) - - # If cryptography is available we'll use it - if can_use_cryptography: - backend = 'cryptography' - elif can_use_pyopenssl: - backend = 'pyopenssl' - - # Fail if no backend has been found - if backend == 'auto': - module.fail_json(msg=("Can't detect any of the required Python libraries " - "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( - MINIMAL_CRYPTOGRAPHY_VERSION, - MINIMAL_PYOPENSSL_VERSION)) - - if backend == 'pyopenssl': - if not PYOPENSSL_FOUND: - module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), - exception=PYOPENSSL_IMP_ERR) - try: - getattr(crypto.X509Req, 'get_extensions') - except AttributeError: - module.fail_json(msg='You need to have PyOpenSSL>=0.15') - - module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') - certificate = CertificateInfoPyOpenSSL(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) - certificate = CertificateInfoCryptography(module) - - result = certificate.get_info() - module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: - module.fail_json(msg=to_native(exc)) - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/crypto/openssl_csr.py b/lib/ansible/modules/crypto/openssl_csr.py deleted file mode 100644 index ea2cf68c2a..0000000000 --- a/lib/ansible/modules/crypto/openssl_csr.py +++ /dev/null @@ -1,1159 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyrigt: (c) 2017, Yanis Guenane <yanis+ansible@guenane.org> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: openssl_csr -version_added: '2.4' -short_description: Generate OpenSSL Certificate Signing Request (CSR) -description: - - This module allows one to (re)generate OpenSSL certificate signing requests. - - It uses the pyOpenSSL python library to interact with openssl. This module supports - the subjectAltName, keyUsage, extendedKeyUsage, basicConstraints and OCSP Must Staple - extensions. - - "Please note that the module regenerates existing CSR if it doesn't match the module's - options, or if it seems to be corrupt. If you are concerned that this could overwrite - your existing CSR, consider using the I(backup) option." - - The module can use the cryptography Python library, or the pyOpenSSL Python - library. By default, it tries to detect which one is available. This can be - overridden with the I(select_crypto_backend) option. Please note that the - PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13." -requirements: - - Either cryptography >= 1.3 - - Or pyOpenSSL >= 0.15 -author: -- Yanis Guenane (@Spredzy) -options: - state: - description: - - Whether the certificate signing request should exist or not, taking action if the state is different from what is stated. - type: str - default: present - choices: [ absent, present ] - digest: - description: - - The digest used when signing the certificate signing request with the private key. - type: str - default: sha256 - privatekey_path: - description: - - The path to the private key to use when signing the certificate signing request. - - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both. - type: path - privatekey_content: - description: - - The content of the private key to use when signing the certificate signing request. - - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both. - type: str - version_added: "2.10" - privatekey_passphrase: - description: - - The passphrase for the private key. - - This is required if the private key is password protected. - type: str - version: - description: - - The version of the certificate signing request. - - "The only allowed value according to L(RFC 2986,https://tools.ietf.org/html/rfc2986#section-4.1) - is 1." - - This option will no longer accept unsupported values from Ansible 2.14 on. - type: int - default: 1 - force: - description: - - Should the certificate signing request be forced regenerated by this ansible module. - type: bool - default: no - path: - description: - - The name of the file into which the generated OpenSSL certificate signing request will be written. - type: path - required: true - subject: - description: - - Key/value pairs that will be present in the subject name field of the certificate signing request. - - If you need to specify more than one value with the same key, use a list as value. - type: dict - version_added: '2.5' - country_name: - description: - - The countryName field of the certificate signing request subject. - type: str - aliases: [ C, countryName ] - state_or_province_name: - description: - - The stateOrProvinceName field of the certificate signing request subject. - type: str - aliases: [ ST, stateOrProvinceName ] - locality_name: - description: - - The localityName field of the certificate signing request subject. - type: str - aliases: [ L, localityName ] - organization_name: - description: - - The organizationName field of the certificate signing request subject. - type: str - aliases: [ O, organizationName ] - organizational_unit_name: - description: - - The organizationalUnitName field of the certificate signing request subject. - type: str - aliases: [ OU, organizationalUnitName ] - common_name: - description: - - The commonName field of the certificate signing request subject. - type: str - aliases: [ CN, commonName ] - email_address: - description: - - The emailAddress field of the certificate signing request subject. - type: str - aliases: [ E, emailAddress ] - subject_alt_name: - description: - - SAN extension to attach to the certificate signing request. - - This can either be a 'comma separated string' or a YAML list. - - Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), - C(otherName) and the ones specific to your CA) - - Note that if no SAN is specified, but a common name, the common - name will be added as a SAN except if C(useCommonNameForSAN) is - set to I(false). - - More at U(https://tools.ietf.org/html/rfc5280#section-4.2.1.6). - type: list - elements: str - aliases: [ subjectAltName ] - subject_alt_name_critical: - description: - - Should the subjectAltName extension be considered as critical. - type: bool - aliases: [ subjectAltName_critical ] - use_common_name_for_san: - description: - - If set to C(yes), the module will fill the common name in for - C(subject_alt_name) with C(DNS:) prefix if no SAN is specified. - type: bool - default: yes - version_added: '2.8' - aliases: [ useCommonNameForSAN ] - key_usage: - description: - - This defines the purpose (e.g. encipherment, signature, certificate signing) - of the key contained in the certificate. - type: list - elements: str - aliases: [ keyUsage ] - key_usage_critical: - description: - - Should the keyUsage extension be considered as critical. - type: bool - aliases: [ keyUsage_critical ] - extended_key_usage: - description: - - Additional restrictions (e.g. client authentication, server authentication) - on the allowed purposes for which the public key may be used. - type: list - elements: str - aliases: [ extKeyUsage, extendedKeyUsage ] - extended_key_usage_critical: - description: - - Should the extkeyUsage extension be considered as critical. - type: bool - aliases: [ extKeyUsage_critical, extendedKeyUsage_critical ] - basic_constraints: - description: - - Indicates basic constraints, such as if the certificate is a CA. - type: list - elements: str - version_added: '2.5' - aliases: [ basicConstraints ] - basic_constraints_critical: - description: - - Should the basicConstraints extension be considered as critical. - type: bool - version_added: '2.5' - aliases: [ basicConstraints_critical ] - ocsp_must_staple: - description: - - Indicates that the certificate should contain the OCSP Must Staple - extension (U(https://tools.ietf.org/html/rfc7633)). - type: bool - version_added: '2.5' - aliases: [ ocspMustStaple ] - ocsp_must_staple_critical: - description: - - Should the OCSP Must Staple extension be considered as critical - - Note that according to the RFC, this extension should not be marked - as critical, as old clients not knowing about OCSP Must Staple - are required to reject such certificates - (see U(https://tools.ietf.org/html/rfc7633#section-4)). - type: bool - version_added: '2.5' - aliases: [ ocspMustStaple_critical ] - 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(pyopenssl). - - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. - - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. - - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. - From that point on, only the C(cryptography) backend will be available. - type: str - default: auto - choices: [ auto, cryptography, pyopenssl ] - version_added: '2.8' - backup: - description: - - Create a backup file including a timestamp so you can get the original - CSR back if you overwrote it with a new one by accident. - type: bool - default: no - version_added: "2.8" - create_subject_key_identifier: - description: - - Create the Subject Key Identifier from the public key. - - "Please note that commercial CAs can ignore the value, respectively use a value of - their own choice instead. Specifying this option is mostly useful for self-signed - certificates or for own CAs." - - Note that this is only supported if the C(cryptography) backend is used! - type: bool - default: no - version_added: "2.9" - subject_key_identifier: - description: - - The subject key identifier as a hex string, where two bytes are separated by colons. - - "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)" - - "Please note that commercial CAs ignore this value, respectively use a value of their - own choice. Specifying this option is mostly useful for self-signed certificates - or for own CAs." - - Note that this option can only be used if I(create_subject_key_identifier) is C(no). - - Note that this is only supported if the C(cryptography) backend is used! - type: str - version_added: "2.9" - authority_key_identifier: - description: - - The authority key identifier as a hex string, where two bytes are separated by colons. - - "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)" - - If specified, I(authority_cert_issuer) must also be specified. - - "Please note that commercial CAs ignore this value, respectively use a value of their - own choice. Specifying this option is mostly useful for self-signed certificates - or for own CAs." - - Note that this is only supported if the C(cryptography) backend is used! - - The C(AuthorityKeyIdentifier) will only be added if at least one of I(authority_key_identifier), - I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. - type: str - version_added: "2.9" - authority_cert_issuer: - description: - - Names that will be present in the authority cert issuer field of the certificate signing request. - - Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName), - C(otherName) and the ones specific to your CA) - - "Example: C(DNS:ca.example.org)" - - If specified, I(authority_key_identifier) must also be specified. - - "Please note that commercial CAs ignore this value, respectively use a value of their - own choice. Specifying this option is mostly useful for self-signed certificates - or for own CAs." - - Note that this is only supported if the C(cryptography) backend is used! - - The C(AuthorityKeyIdentifier) will only be added if at least one of I(authority_key_identifier), - I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. - type: list - elements: str - version_added: "2.9" - authority_cert_serial_number: - description: - - The authority cert serial number. - - Note that this is only supported if the C(cryptography) backend is used! - - "Please note that commercial CAs ignore this value, respectively use a value of their - own choice. Specifying this option is mostly useful for self-signed certificates - or for own CAs." - - The C(AuthorityKeyIdentifier) will only be added if at least one of I(authority_key_identifier), - I(authority_cert_issuer) and I(authority_cert_serial_number) is specified. - type: int - version_added: "2.9" - return_content: - description: - - If set to C(yes), will return the (current or generated) CSR's content as I(csr). - type: bool - default: no - version_added: "2.10" -extends_documentation_fragment: -- files -notes: - - If the certificate signing request already exists it will be checked whether subjectAltName, - keyUsage, extendedKeyUsage and basicConstraints only contain the requested values, whether - OCSP Must Staple is as requested, and if the request was signed by the given private key. -seealso: -- module: openssl_certificate -- module: openssl_dhparam -- module: openssl_pkcs12 -- module: openssl_privatekey -- module: openssl_publickey -''' - -EXAMPLES = r''' -- name: Generate an OpenSSL Certificate Signing Request - openssl_csr: - path: /etc/ssl/csr/www.ansible.com.csr - privatekey_path: /etc/ssl/private/ansible.com.pem - common_name: www.ansible.com - -- name: Generate an OpenSSL Certificate Signing Request with an inline key - openssl_csr: - path: /etc/ssl/csr/www.ansible.com.csr - privatekey_content: "{{ private_key_content }}" - common_name: www.ansible.com - -- name: 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 - common_name: www.ansible.com - -- name: 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 - country_name: FR - organization_name: Ansible - email_address: jdoe@ansible.com - common_name: www.ansible.com - -- name: 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 - subject_alt_name: 'DNS:www.ansible.com,DNS:m.ansible.com' - -- name: Generate an OpenSSL CSR with subjectAltName extension with dynamic list - openssl_csr: - path: /etc/ssl/csr/www.ansible.com.csr - privatekey_path: /etc/ssl/private/ansible.com.pem - subject_alt_name: "{{ item.value | map('regex_replace', '^', 'DNS:') | list }}" - with_dict: - dns_server: - - www.ansible.com - - m.ansible.com - -- name: Force regenerate an OpenSSL Certificate Signing Request - openssl_csr: - path: /etc/ssl/csr/www.ansible.com.csr - privatekey_path: /etc/ssl/private/ansible.com.pem - force: yes - common_name: www.ansible.com - -- name: Generate an OpenSSL Certificate Signing Request with special key usages - openssl_csr: - path: /etc/ssl/csr/www.ansible.com.csr - privatekey_path: /etc/ssl/private/ansible.com.pem - common_name: www.ansible.com - key_usage: - - digitalSignature - - keyAgreement - extended_key_usage: - - clientAuth - -- name: Generate an OpenSSL Certificate Signing Request with OCSP Must Staple - openssl_csr: - path: /etc/ssl/csr/www.ansible.com.csr - privatekey_path: /etc/ssl/private/ansible.com.pem - common_name: www.ansible.com - ocsp_must_staple: yes -''' - -RETURN = r''' -privatekey: - description: - - Path to the TLS/SSL private key the CSR was generated for - - Will be C(none) if the private key has been provided in I(privatekey_content). - returned: changed or success - type: str - sample: /etc/ssl/private/ansible.com.pem -filename: - description: Path to the generated Certificate Signing Request - returned: changed or success - type: str - sample: /etc/ssl/csr/www.ansible.com.csr -subject: - description: A list of the subject tuples attached to the CSR - returned: changed or success - type: list - elements: list - sample: "[('CN', 'www.ansible.com'), ('O', 'Ansible')]" -subjectAltName: - description: The alternative names this CSR is valid for - returned: changed or success - type: list - elements: str - sample: [ 'DNS:www.ansible.com', 'DNS:m.ansible.com' ] -keyUsage: - description: Purpose for which the public key may be used - returned: changed or success - type: list - elements: str - sample: [ 'digitalSignature', 'keyAgreement' ] -extendedKeyUsage: - description: Additional restriction on the public key purposes - returned: changed or success - type: list - elements: str - sample: [ 'clientAuth' ] -basicConstraints: - description: Indicates if the certificate belongs to a CA - returned: changed or success - type: list - elements: str - sample: ['CA:TRUE', 'pathLenConstraint:0'] -ocsp_must_staple: - description: Indicates whether the certificate has the OCSP - Must Staple feature enabled - returned: changed or success - type: bool - sample: false -backup_file: - description: Name of backup file created. - returned: changed and if I(backup) is C(yes) - type: str - sample: /path/to/www.ansible.com.csr.2019-03-09@11:22~ -csr: - description: The (current or generated) CSR's content. - returned: if I(state) is C(present) and I(return_content) is C(yes) - type: str - version_added: "2.10" -''' - -import abc -import binascii -import os -import traceback -from distutils.version import LooseVersion - -from ansible.module_utils import crypto as crypto_utils -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native, to_bytes, to_text -from ansible.module_utils.compat import ipaddress as compat_ipaddress - -MINIMAL_PYOPENSSL_VERSION = '0.15' -MINIMAL_CRYPTOGRAPHY_VERSION = '1.3' - -PYOPENSSL_IMP_ERR = None -try: - import OpenSSL - from OpenSSL import crypto - PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) -except ImportError: - PYOPENSSL_IMP_ERR = traceback.format_exc() - PYOPENSSL_FOUND = False -else: - PYOPENSSL_FOUND = True - if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000: - # OpenSSL 1.1.0 or newer - OPENSSL_MUST_STAPLE_NAME = b"tlsfeature" - OPENSSL_MUST_STAPLE_VALUE = b"status_request" - else: - # OpenSSL 1.0.x or older - OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24" - OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05" - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - import cryptography.x509 - import cryptography.x509.oid - import cryptography.exceptions - import cryptography.hazmat.backends - import cryptography.hazmat.primitives.serialization - import cryptography.hazmat.primitives.hashes - CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - CRYPTOGRAPHY_MUST_STAPLE_NAME = cryptography.x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24") - CRYPTOGRAPHY_MUST_STAPLE_VALUE = b"\x30\x03\x02\x01\x05" - - -class CertificateSigningRequestError(crypto_utils.OpenSSLObjectError): - pass - - -class CertificateSigningRequestBase(crypto_utils.OpenSSLObject): - - def __init__(self, module): - super(CertificateSigningRequestBase, self).__init__( - module.params['path'], - module.params['state'], - module.params['force'], - module.check_mode - ) - self.digest = module.params['digest'] - self.privatekey_path = module.params['privatekey_path'] - self.privatekey_content = module.params['privatekey_content'] - if self.privatekey_content is not None: - self.privatekey_content = self.privatekey_content.encode('utf-8') - self.privatekey_passphrase = module.params['privatekey_passphrase'] - self.version = module.params['version'] - self.subjectAltName = module.params['subject_alt_name'] - self.subjectAltName_critical = module.params['subject_alt_name_critical'] - self.keyUsage = module.params['key_usage'] - self.keyUsage_critical = module.params['key_usage_critical'] - self.extendedKeyUsage = module.params['extended_key_usage'] - self.extendedKeyUsage_critical = module.params['extended_key_usage_critical'] - self.basicConstraints = module.params['basic_constraints'] - self.basicConstraints_critical = module.params['basic_constraints_critical'] - self.ocspMustStaple = module.params['ocsp_must_staple'] - self.ocspMustStaple_critical = module.params['ocsp_must_staple_critical'] - self.create_subject_key_identifier = module.params['create_subject_key_identifier'] - self.subject_key_identifier = module.params['subject_key_identifier'] - self.authority_key_identifier = module.params['authority_key_identifier'] - self.authority_cert_issuer = module.params['authority_cert_issuer'] - self.authority_cert_serial_number = module.params['authority_cert_serial_number'] - self.request = None - self.privatekey = None - self.csr_bytes = None - self.return_content = module.params['return_content'] - - if self.create_subject_key_identifier and self.subject_key_identifier is not None: - module.fail_json(msg='subject_key_identifier cannot be specified if create_subject_key_identifier is true') - - self.backup = module.params['backup'] - self.backup_file = None - - self.subject = [ - ('C', module.params['country_name']), - ('ST', module.params['state_or_province_name']), - ('L', module.params['locality_name']), - ('O', module.params['organization_name']), - ('OU', module.params['organizational_unit_name']), - ('CN', module.params['common_name']), - ('emailAddress', module.params['email_address']), - ] - - if module.params['subject']: - self.subject = self.subject + crypto_utils.parse_name_field(module.params['subject']) - self.subject = [(entry[0], entry[1]) for entry in self.subject if entry[1]] - - if not self.subjectAltName and module.params['use_common_name_for_san']: - for sub in self.subject: - if sub[0] in ('commonName', 'CN'): - self.subjectAltName = ['DNS:%s' % sub[1]] - break - - if self.subject_key_identifier is not None: - try: - self.subject_key_identifier = binascii.unhexlify(self.subject_key_identifier.replace(':', '')) - except Exception as e: - raise CertificateSigningRequestError('Cannot parse subject_key_identifier: {0}'.format(e)) - - if self.authority_key_identifier is not None: - try: - self.authority_key_identifier = binascii.unhexlify(self.authority_key_identifier.replace(':', '')) - except Exception as e: - raise CertificateSigningRequestError('Cannot parse authority_key_identifier: {0}'.format(e)) - - @abc.abstractmethod - def _generate_csr(self): - pass - - def generate(self, module): - '''Generate the certificate signing request.''' - if not self.check(module, perms_required=False) or self.force: - result = self._generate_csr() - if self.backup: - self.backup_file = module.backup_local(self.path) - if self.return_content: - self.csr_bytes = result - crypto_utils.write_file(module, result) - self.changed = True - - file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, False): - self.changed = True - - @abc.abstractmethod - def _load_private_key(self): - pass - - @abc.abstractmethod - def _check_csr(self): - pass - - def check(self, module, perms_required=True): - """Ensure the resource is in its desired state.""" - state_and_perms = super(CertificateSigningRequestBase, self).check(module, perms_required) - - self._load_private_key() - - if not state_and_perms: - return False - - return self._check_csr() - - def remove(self, module): - if self.backup: - self.backup_file = module.backup_local(self.path) - super(CertificateSigningRequestBase, self).remove(module) - - def dump(self): - '''Serialize the object into a dictionary.''' - - result = { - 'privatekey': self.privatekey_path, - 'filename': self.path, - 'subject': self.subject, - 'subjectAltName': self.subjectAltName, - 'keyUsage': self.keyUsage, - 'extendedKeyUsage': self.extendedKeyUsage, - 'basicConstraints': self.basicConstraints, - 'ocspMustStaple': self.ocspMustStaple, - 'changed': self.changed - } - if self.backup_file: - result['backup_file'] = self.backup_file - if self.return_content: - if self.csr_bytes is None: - self.csr_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - result['csr'] = self.csr_bytes.decode('utf-8') if self.csr_bytes else None - - return result - - -class CertificateSigningRequestPyOpenSSL(CertificateSigningRequestBase): - - def __init__(self, module): - if module.params['create_subject_key_identifier']: - module.fail_json(msg='You cannot use create_subject_key_identifier with the pyOpenSSL backend!') - for o in ('subject_key_identifier', 'authority_key_identifier', 'authority_cert_issuer', 'authority_cert_serial_number'): - if module.params[o] is not None: - module.fail_json(msg='You cannot use {0} with the pyOpenSSL backend!'.format(o)) - super(CertificateSigningRequestPyOpenSSL, self).__init__(module) - - def _generate_csr(self): - req = crypto.X509Req() - req.set_version(self.version - 1) - subject = req.get_subject() - for entry in self.subject: - if entry[1] is not None: - # Workaround for https://github.com/pyca/pyopenssl/issues/165 - nid = OpenSSL._util.lib.OBJ_txt2nid(to_bytes(entry[0])) - if nid == 0: - raise CertificateSigningRequestError('Unknown subject field identifier "{0}"'.format(entry[0])) - res = OpenSSL._util.lib.X509_NAME_add_entry_by_NID(subject._name, nid, OpenSSL._util.lib.MBSTRING_UTF8, to_bytes(entry[1]), -1, -1, 0) - if res == 0: - raise CertificateSigningRequestError('Invalid value for subject field identifier "{0}": {1}'.format(entry[0], entry[1])) - - extensions = [] - if self.subjectAltName: - altnames = ', '.join(self.subjectAltName) - try: - extensions.append(crypto.X509Extension(b"subjectAltName", self.subjectAltName_critical, altnames.encode('ascii'))) - except OpenSSL.crypto.Error as e: - raise CertificateSigningRequestError( - 'Error while parsing Subject Alternative Names {0} (check for missing type prefix, such as "DNS:"!): {1}'.format( - ', '.join(["{0}".format(san) for san in self.subjectAltName]), str(e) - ) - ) - - if self.keyUsage: - usages = ', '.join(self.keyUsage) - extensions.append(crypto.X509Extension(b"keyUsage", self.keyUsage_critical, usages.encode('ascii'))) - - if self.extendedKeyUsage: - usages = ', '.join(self.extendedKeyUsage) - extensions.append(crypto.X509Extension(b"extendedKeyUsage", self.extendedKeyUsage_critical, usages.encode('ascii'))) - - if self.basicConstraints: - usages = ', '.join(self.basicConstraints) - extensions.append(crypto.X509Extension(b"basicConstraints", self.basicConstraints_critical, usages.encode('ascii'))) - - if self.ocspMustStaple: - extensions.append(crypto.X509Extension(OPENSSL_MUST_STAPLE_NAME, self.ocspMustStaple_critical, OPENSSL_MUST_STAPLE_VALUE)) - - if extensions: - req.add_extensions(extensions) - - req.set_pubkey(self.privatekey) - req.sign(self.privatekey, self.digest) - self.request = req - - return crypto.dump_certificate_request(crypto.FILETYPE_PEM, self.request) - - def _load_private_key(self): - try: - self.privatekey = crypto_utils.load_privatekey( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase - ) - except crypto_utils.OpenSSLBadPassphraseError as exc: - raise CertificateSigningRequestError(exc) - - def _normalize_san(self, san): - # Apparently OpenSSL returns 'IP address' not 'IP' as specifier when converting the subjectAltName to string - # although it won't accept this specifier when generating the CSR. (https://github.com/openssl/openssl/issues/4004) - if san.startswith('IP Address:'): - san = 'IP:' + san[len('IP Address:'):] - if san.startswith('IP:'): - ip = compat_ipaddress.ip_address(san[3:]) - san = 'IP:{0}'.format(ip.compressed) - return san - - def _check_csr(self): - def _check_subject(csr): - subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in self.subject] - current_subject = [(OpenSSL._util.lib.OBJ_txt2nid(to_bytes(sub[0])), to_bytes(sub[1])) for sub in csr.get_subject().get_components()] - if not set(subject) == set(current_subject): - return False - - return True - - def _check_subjectAltName(extensions): - altnames_ext = next((ext for ext in extensions if ext.get_short_name() == b'subjectAltName'), '') - altnames = [self._normalize_san(altname.strip()) for altname in - to_text(altnames_ext, errors='surrogate_or_strict').split(',') if altname.strip()] - if self.subjectAltName: - if (set(altnames) != set([self._normalize_san(to_text(name)) for name in self.subjectAltName]) or - altnames_ext.get_critical() != self.subjectAltName_critical): - return False - else: - if altnames: - return False - - return True - - def _check_keyUsage_(extensions, extName, expected, critical): - usages_ext = [ext for ext in extensions if ext.get_short_name() == extName] - if (not usages_ext and expected) or (usages_ext and not expected): - return False - elif not usages_ext and not expected: - return True - else: - current = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage.strip())) for usage in str(usages_ext[0]).split(',')] - expected = [OpenSSL._util.lib.OBJ_txt2nid(to_bytes(usage)) for usage in expected] - return set(current) == set(expected) and usages_ext[0].get_critical() == critical - - def _check_keyUsage(extensions): - usages_ext = [ext for ext in extensions if ext.get_short_name() == b'keyUsage'] - if (not usages_ext and self.keyUsage) or (usages_ext and not self.keyUsage): - return False - elif not usages_ext and not self.keyUsage: - return True - else: - # OpenSSL._util.lib.OBJ_txt2nid() always returns 0 for all keyUsage values - # (since keyUsage has a fixed bitfield for these values and is not extensible). - # Therefore, we create an extension for the wanted values, and compare the - # data of the extensions (which is the serialized bitfield). - expected_ext = crypto.X509Extension(b"keyUsage", False, ', '.join(self.keyUsage).encode('ascii')) - return usages_ext[0].get_data() == expected_ext.get_data() and usages_ext[0].get_critical() == self.keyUsage_critical - - def _check_extenededKeyUsage(extensions): - return _check_keyUsage_(extensions, b'extendedKeyUsage', self.extendedKeyUsage, self.extendedKeyUsage_critical) - - def _check_basicConstraints(extensions): - return _check_keyUsage_(extensions, b'basicConstraints', self.basicConstraints, self.basicConstraints_critical) - - def _check_ocspMustStaple(extensions): - oms_ext = [ext for ext in extensions if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE] - if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000: - # Older versions of libssl don't know about OCSP Must Staple - oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05']) - if self.ocspMustStaple: - return len(oms_ext) > 0 and oms_ext[0].get_critical() == self.ocspMustStaple_critical - else: - return len(oms_ext) == 0 - - def _check_extensions(csr): - extensions = csr.get_extensions() - return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and - _check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and - _check_ocspMustStaple(extensions)) - - def _check_signature(csr): - try: - return csr.verify(self.privatekey) - except crypto.Error: - return False - - try: - csr = crypto_utils.load_certificate_request(self.path, backend='pyopenssl') - except Exception as dummy: - return False - - return _check_subject(csr) and _check_extensions(csr) and _check_signature(csr) - - -class CertificateSigningRequestCryptography(CertificateSigningRequestBase): - - def __init__(self, module): - super(CertificateSigningRequestCryptography, self).__init__(module) - self.cryptography_backend = cryptography.hazmat.backends.default_backend() - self.module = module - if self.version != 1: - module.warn('The cryptography backend only supports version 1. (The only valid value according to RFC 2986.)') - - def _generate_csr(self): - csr = cryptography.x509.CertificateSigningRequestBuilder() - try: - csr = csr.subject_name(cryptography.x509.Name([ - cryptography.x509.NameAttribute(crypto_utils.cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject - ])) - except ValueError as e: - raise CertificateSigningRequestError(e) - - if self.subjectAltName: - csr = csr.add_extension(cryptography.x509.SubjectAlternativeName([ - crypto_utils.cryptography_get_name(name) for name in self.subjectAltName - ]), critical=self.subjectAltName_critical) - - if self.keyUsage: - params = crypto_utils.cryptography_parse_key_usage_params(self.keyUsage) - csr = csr.add_extension(cryptography.x509.KeyUsage(**params), critical=self.keyUsage_critical) - - if self.extendedKeyUsage: - usages = [crypto_utils.cryptography_name_to_oid(usage) for usage in self.extendedKeyUsage] - csr = csr.add_extension(cryptography.x509.ExtendedKeyUsage(usages), critical=self.extendedKeyUsage_critical) - - if self.basicConstraints: - params = {} - ca, path_length = crypto_utils.cryptography_get_basic_constraints(self.basicConstraints) - csr = csr.add_extension(cryptography.x509.BasicConstraints(ca, path_length), critical=self.basicConstraints_critical) - - if self.ocspMustStaple: - try: - # This only works with cryptography >= 2.1 - csr = csr.add_extension(cryptography.x509.TLSFeature([cryptography.x509.TLSFeatureType.status_request]), critical=self.ocspMustStaple_critical) - except AttributeError as dummy: - csr = csr.add_extension( - cryptography.x509.UnrecognizedExtension(CRYPTOGRAPHY_MUST_STAPLE_NAME, CRYPTOGRAPHY_MUST_STAPLE_VALUE), - critical=self.ocspMustStaple_critical - ) - - if self.create_subject_key_identifier: - csr = csr.add_extension( - cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()), - critical=False - ) - elif self.subject_key_identifier is not None: - csr = csr.add_extension(cryptography.x509.SubjectKeyIdentifier(self.subject_key_identifier), critical=False) - - if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None: - issuers = None - if self.authority_cert_issuer is not None: - issuers = [crypto_utils.cryptography_get_name(n) for n in self.authority_cert_issuer] - csr = csr.add_extension( - cryptography.x509.AuthorityKeyIdentifier(self.authority_key_identifier, issuers, self.authority_cert_serial_number), - critical=False - ) - - digest = None - if crypto_utils.cryptography_key_needs_digest_for_signing(self.privatekey): - if self.digest == 'sha256': - digest = cryptography.hazmat.primitives.hashes.SHA256() - elif self.digest == 'sha384': - digest = cryptography.hazmat.primitives.hashes.SHA384() - elif self.digest == 'sha512': - digest = cryptography.hazmat.primitives.hashes.SHA512() - elif self.digest == 'sha1': - digest = cryptography.hazmat.primitives.hashes.SHA1() - elif self.digest == 'md5': - digest = cryptography.hazmat.primitives.hashes.MD5() - # FIXME - else: - raise CertificateSigningRequestError('Unsupported digest "{0}"'.format(self.digest)) - try: - self.request = csr.sign(self.privatekey, digest, self.cryptography_backend) - except TypeError as e: - if str(e) == 'Algorithm must be a registered hash algorithm.' and digest is None: - self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') - raise - - return self.request.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM) - - def _load_private_key(self): - try: - if self.privatekey_content is not None: - content = self.privatekey_content - else: - with open(self.privatekey_path, 'rb') as f: - content = f.read() - self.privatekey = cryptography.hazmat.primitives.serialization.load_pem_private_key( - content, - None if self.privatekey_passphrase is None else to_bytes(self.privatekey_passphrase), - backend=self.cryptography_backend - ) - except Exception as e: - raise CertificateSigningRequestError(e) - - def _check_csr(self): - def _check_subject(csr): - subject = [(crypto_utils.cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.subject] - current_subject = [(sub.oid, sub.value) for sub in csr.subject] - return set(subject) == set(current_subject) - - def _find_extension(extensions, exttype): - return next( - (ext for ext in extensions if isinstance(ext.value, exttype)), - None - ) - - def _check_subjectAltName(extensions): - current_altnames_ext = _find_extension(extensions, cryptography.x509.SubjectAlternativeName) - current_altnames = [str(altname) for altname in current_altnames_ext.value] if current_altnames_ext else [] - altnames = [str(crypto_utils.cryptography_get_name(altname)) for altname in self.subjectAltName] if self.subjectAltName else [] - if set(altnames) != set(current_altnames): - return False - if altnames: - if current_altnames_ext.critical != self.subjectAltName_critical: - return False - return True - - def _check_keyUsage(extensions): - current_keyusage_ext = _find_extension(extensions, cryptography.x509.KeyUsage) - if not self.keyUsage: - return current_keyusage_ext is None - elif current_keyusage_ext is None: - return False - params = crypto_utils.cryptography_parse_key_usage_params(self.keyUsage) - for param in params: - if getattr(current_keyusage_ext.value, '_' + param) != params[param]: - return False - if current_keyusage_ext.critical != self.keyUsage_critical: - return False - return True - - def _check_extenededKeyUsage(extensions): - current_usages_ext = _find_extension(extensions, cryptography.x509.ExtendedKeyUsage) - current_usages = [str(usage) for usage in current_usages_ext.value] if current_usages_ext else [] - usages = [str(crypto_utils.cryptography_name_to_oid(usage)) for usage in self.extendedKeyUsage] if self.extendedKeyUsage else [] - if set(current_usages) != set(usages): - return False - if usages: - if current_usages_ext.critical != self.extendedKeyUsage_critical: - return False - return True - - def _check_basicConstraints(extensions): - bc_ext = _find_extension(extensions, cryptography.x509.BasicConstraints) - current_ca = bc_ext.value.ca if bc_ext else False - current_path_length = bc_ext.value.path_length if bc_ext else None - ca, path_length = crypto_utils.cryptography_get_basic_constraints(self.basicConstraints) - # Check CA flag - if ca != current_ca: - return False - # Check path length - if path_length != current_path_length: - return False - # Check criticality - if self.basicConstraints: - if bc_ext.critical != self.basicConstraints_critical: - return False - return True - - def _check_ocspMustStaple(extensions): - try: - # This only works with cryptography >= 2.1 - tlsfeature_ext = _find_extension(extensions, cryptography.x509.TLSFeature) - has_tlsfeature = True - except AttributeError as dummy: - tlsfeature_ext = next( - (ext for ext in extensions if ext.value.oid == CRYPTOGRAPHY_MUST_STAPLE_NAME), - None - ) - has_tlsfeature = False - if self.ocspMustStaple: - if not tlsfeature_ext or tlsfeature_ext.critical != self.ocspMustStaple_critical: - return False - if has_tlsfeature: - return cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value - else: - return tlsfeature_ext.value.value == CRYPTOGRAPHY_MUST_STAPLE_VALUE - else: - return tlsfeature_ext is None - - def _check_subject_key_identifier(extensions): - ext = _find_extension(extensions, cryptography.x509.SubjectKeyIdentifier) - if self.create_subject_key_identifier or self.subject_key_identifier is not None: - if not ext or ext.critical: - return False - if self.create_subject_key_identifier: - digest = cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()).digest - return ext.value.digest == digest - else: - return ext.value.digest == self.subject_key_identifier - else: - return ext is None - - def _check_authority_key_identifier(extensions): - ext = _find_extension(extensions, cryptography.x509.AuthorityKeyIdentifier) - if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None: - if not ext or ext.critical: - return False - aci = None - csr_aci = None - if self.authority_cert_issuer is not None: - aci = [str(crypto_utils.cryptography_get_name(n)) for n in self.authority_cert_issuer] - if ext.value.authority_cert_issuer is not None: - csr_aci = [str(n) for n in ext.value.authority_cert_issuer] - return (ext.value.key_identifier == self.authority_key_identifier - and csr_aci == aci - and ext.value.authority_cert_serial_number == self.authority_cert_serial_number) - else: - return ext is None - - def _check_extensions(csr): - extensions = csr.extensions - return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and - _check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and - _check_ocspMustStaple(extensions) and _check_subject_key_identifier(extensions) and - _check_authority_key_identifier(extensions)) - - def _check_signature(csr): - if not csr.is_signature_valid: - return False - # To check whether public key of CSR belongs to private key, - # encode both public keys and compare PEMs. - key_a = csr.public_key().public_bytes( - cryptography.hazmat.primitives.serialization.Encoding.PEM, - cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo - ) - key_b = self.privatekey.public_key().public_bytes( - cryptography.hazmat.primitives.serialization.Encoding.PEM, - cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo - ) - return key_a == key_b - - try: - csr = crypto_utils.load_certificate_request(self.path, backend='cryptography') - except Exception as dummy: - return False - - return _check_subject(csr) and _check_extensions(csr) and _check_signature(csr) - - -def main(): - module = AnsibleModule( - argument_spec=dict( - state=dict(type='str', default='present', choices=['absent', 'present']), - digest=dict(type='str', default='sha256'), - privatekey_path=dict(type='path'), - privatekey_content=dict(type='str'), - privatekey_passphrase=dict(type='str', no_log=True), - version=dict(type='int', default=1), - force=dict(type='bool', default=False), - path=dict(type='path', required=True), - subject=dict(type='dict'), - country_name=dict(type='str', aliases=['C', 'countryName']), - state_or_province_name=dict(type='str', aliases=['ST', 'stateOrProvinceName']), - locality_name=dict(type='str', aliases=['L', 'localityName']), - organization_name=dict(type='str', aliases=['O', 'organizationName']), - organizational_unit_name=dict(type='str', aliases=['OU', 'organizationalUnitName']), - common_name=dict(type='str', aliases=['CN', 'commonName']), - email_address=dict(type='str', aliases=['E', 'emailAddress']), - subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName']), - subject_alt_name_critical=dict(type='bool', default=False, aliases=['subjectAltName_critical']), - use_common_name_for_san=dict(type='bool', default=True, aliases=['useCommonNameForSAN']), - key_usage=dict(type='list', elements='str', aliases=['keyUsage']), - key_usage_critical=dict(type='bool', default=False, aliases=['keyUsage_critical']), - extended_key_usage=dict(type='list', elements='str', aliases=['extKeyUsage', 'extendedKeyUsage']), - extended_key_usage_critical=dict(type='bool', default=False, aliases=['extKeyUsage_critical', 'extendedKeyUsage_critical']), - basic_constraints=dict(type='list', elements='str', aliases=['basicConstraints']), - basic_constraints_critical=dict(type='bool', default=False, aliases=['basicConstraints_critical']), - ocsp_must_staple=dict(type='bool', default=False, aliases=['ocspMustStaple']), - ocsp_must_staple_critical=dict(type='bool', default=False, aliases=['ocspMustStaple_critical']), - backup=dict(type='bool', default=False), - create_subject_key_identifier=dict(type='bool', default=False), - subject_key_identifier=dict(type='str'), - authority_key_identifier=dict(type='str'), - authority_cert_issuer=dict(type='list', elements='str'), - authority_cert_serial_number=dict(type='int'), - select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), - return_content=dict(type='bool', default=False), - ), - required_together=[('authority_cert_issuer', 'authority_cert_serial_number')], - required_if=[('state', 'present', ['privatekey_path', 'privatekey_content'], True)], - mutually_exclusive=( - ['privatekey_path', 'privatekey_content'], - ), - add_file_common_args=True, - supports_check_mode=True, - ) - - if module.params['version'] != 1: - module.deprecate('The version option will only support allowed values from Ansible 2.14 on. ' - 'Currently, only the value 1 is allowed by RFC 2986', version='2.14') - - base_dir = os.path.dirname(module.params['path']) or '.' - if not os.path.isdir(base_dir): - module.fail_json(name=base_dir, msg='The directory %s does not exist or the file is not a directory' % base_dir) - - 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_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) - - # First try cryptography, then pyOpenSSL - if can_use_cryptography: - backend = 'cryptography' - elif can_use_pyopenssl: - backend = 'pyopenssl' - - # Success? - if backend == 'auto': - module.fail_json(msg=("Can't detect any of the required Python libraries " - "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( - MINIMAL_CRYPTOGRAPHY_VERSION, - MINIMAL_PYOPENSSL_VERSION)) - try: - if backend == 'pyopenssl': - if not PYOPENSSL_FOUND: - module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), - exception=PYOPENSSL_IMP_ERR) - try: - getattr(crypto.X509Req, 'get_extensions') - except AttributeError: - module.fail_json(msg='You need to have PyOpenSSL>=0.15 to generate CSRs') - - module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') - csr = CertificateSigningRequestPyOpenSSL(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) - csr = CertificateSigningRequestCryptography(module) - - if module.params['state'] == 'present': - if module.check_mode: - result = csr.dump() - result['changed'] = module.params['force'] or not csr.check(module) - module.exit_json(**result) - - csr.generate(module) - - else: - if module.check_mode: - result = csr.dump() - result['changed'] = os.path.exists(module.params['path']) - module.exit_json(**result) - - csr.remove(module) - - result = csr.dump() - module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: - module.fail_json(msg=to_native(exc)) - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/crypto/openssl_csr_info.py b/lib/ansible/modules/crypto/openssl_csr_info.py deleted file mode 100644 index 713ee33808..0000000000 --- a/lib/ansible/modules/crypto/openssl_csr_info.py +++ /dev/null @@ -1,667 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org> -# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: openssl_csr_info -version_added: '2.8' -short_description: Provide information of OpenSSL Certificate Signing Requests (CSR) -description: - - This module allows one to query information on OpenSSL Certificate Signing Requests (CSR). - - In case the CSR signature cannot be validated, the module will fail. In this case, all return - variables are still returned. - - It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the - cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements) - cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with - C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 - and will be removed in Ansible 2.13. -requirements: - - PyOpenSSL >= 0.15 or cryptography >= 1.3 -author: - - Felix Fontein (@felixfontein) - - Yanis Guenane (@Spredzy) -options: - path: - description: - - Remote absolute path where the CSR file is loaded from. - - Either I(path) or I(content) must be specified, but not both. - type: path - content: - description: - - Content of the CSR file. - - Either I(path) or I(content) must be specified, but not both. - type: str - version_added: "2.10" - 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(pyopenssl). - - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. - - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. - - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. - From that point on, only the C(cryptography) backend will be available. - type: str - default: auto - choices: [ auto, cryptography, pyopenssl ] - -seealso: -- module: openssl_csr -''' - -EXAMPLES = r''' -- name: Generate an OpenSSL Certificate Signing Request - openssl_csr: - path: /etc/ssl/csr/www.ansible.com.csr - privatekey_path: /etc/ssl/private/ansible.com.pem - common_name: www.ansible.com - -- name: Get information on the CSR - openssl_csr_info: - path: /etc/ssl/csr/www.ansible.com.csr - register: result - -- name: Dump information - debug: - var: result -''' - -RETURN = r''' -signature_valid: - description: - - Whether the CSR's signature is valid. - - In case the check returns C(no), the module will fail. - returned: success - type: bool -basic_constraints: - description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present. - returned: success - type: list - elements: str - sample: "[CA:TRUE, pathlen:1]" -basic_constraints_critical: - description: Whether the C(basic_constraints) extension is critical. - returned: success - type: bool -extended_key_usage: - description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present. - returned: success - type: list - elements: str - sample: "[Biometric Info, DVCS, Time Stamping]" -extended_key_usage_critical: - description: Whether the C(extended_key_usage) extension is critical. - returned: success - type: bool -extensions_by_oid: - description: Returns a dictionary for every extension OID - returned: success - type: dict - contains: - critical: - description: Whether the extension is critical. - returned: success - type: bool - value: - description: The Base64 encoded value (in DER format) of the extension - returned: success - type: str - sample: "MAMCAQU=" - sample: '{"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}' -key_usage: - description: Entries in the C(key_usage) extension, or C(none) if extension is not present. - returned: success - type: str - sample: "[Key Agreement, Data Encipherment]" -key_usage_critical: - description: Whether the C(key_usage) extension is critical. - returned: success - type: bool -subject_alt_name: - description: Entries in the C(subject_alt_name) extension, or C(none) if extension is not present. - returned: success - type: list - elements: str - sample: "[DNS:www.ansible.com, IP:1.2.3.4]" -subject_alt_name_critical: - description: Whether the C(subject_alt_name) extension is critical. - returned: success - type: bool -ocsp_must_staple: - description: C(yes) if the OCSP Must Staple extension is present, C(none) otherwise. - returned: success - type: bool -ocsp_must_staple_critical: - description: Whether the C(ocsp_must_staple) extension is critical. - returned: success - type: bool -subject: - description: - - The CSR's subject as a dictionary. - - Note that for repeated values, only the last one will be returned. - returned: success - type: dict - sample: '{"commonName": "www.example.com", "emailAddress": "test@example.com"}' -subject_ordered: - description: The CSR's subject as an ordered list of tuples. - returned: success - type: list - elements: list - sample: '[["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]' - version_added: "2.9" -public_key: - description: CSR's public key in PEM format - returned: success - type: str - sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..." -public_key_fingerprints: - description: - - Fingerprints of CSR'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..." -subject_key_identifier: - description: - - The CSR's subject key identifier. - - The identifier is returned in hexadecimal, with C(:) used to separate bytes. - - Is C(none) if the C(SubjectKeyIdentifier) extension is not present. - returned: success and if the pyOpenSSL backend is I(not) used - type: str - sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' - version_added: "2.9" -authority_key_identifier: - description: - - The CSR's authority key identifier. - - The identifier is returned in hexadecimal, with C(:) used to separate bytes. - - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. - returned: success and if the pyOpenSSL backend is I(not) used - type: str - sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33' - version_added: "2.9" -authority_cert_issuer: - description: - - The CSR's authority cert issuer as a list of general names. - - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. - returned: success and if the pyOpenSSL backend is I(not) used - type: list - elements: str - sample: "[DNS:www.ansible.com, IP:1.2.3.4]" - version_added: "2.9" -authority_cert_serial_number: - description: - - The CSR's authority cert serial number. - - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present. - returned: success and if the pyOpenSSL backend is I(not) used - type: int - sample: '12345' - version_added: "2.9" -''' - - -import abc -import binascii -import os -import traceback -from distutils.version import LooseVersion - -from ansible.module_utils import crypto as crypto_utils -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native, to_text, to_bytes -from ansible.module_utils.compat import ipaddress as compat_ipaddress - -MINIMAL_CRYPTOGRAPHY_VERSION = '1.3' -MINIMAL_PYOPENSSL_VERSION = '0.15' - -PYOPENSSL_IMP_ERR = None -try: - import OpenSSL - from OpenSSL import crypto - PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) - if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000: - # OpenSSL 1.1.0 or newer - OPENSSL_MUST_STAPLE_NAME = b"tlsfeature" - OPENSSL_MUST_STAPLE_VALUE = b"status_request" - else: - # OpenSSL 1.0.x or older - OPENSSL_MUST_STAPLE_NAME = b"1.3.6.1.5.5.7.1.24" - OPENSSL_MUST_STAPLE_VALUE = b"DER:30:03:02:01:05" -except ImportError: - PYOPENSSL_IMP_ERR = traceback.format_exc() - PYOPENSSL_FOUND = False -else: - PYOPENSSL_FOUND = True - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - from cryptography import x509 - from cryptography.hazmat.primitives import serialization - CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - - -TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" - - -class CertificateSigningRequestInfo(crypto_utils.OpenSSLObject): - def __init__(self, module, backend): - super(CertificateSigningRequestInfo, self).__init__( - module.params['path'] or '', - 'present', - False, - module.check_mode, - ) - self.backend = backend - self.module = module - self.content = module.params['content'] - if self.content is not None: - self.content = self.content.encode('utf-8') - - def generate(self): - # Empty method because crypto_utils.OpenSSLObject wants this - pass - - def dump(self): - # Empty method because crypto_utils.OpenSSLObject wants this - pass - - @abc.abstractmethod - def _get_subject_ordered(self): - pass - - @abc.abstractmethod - def _get_key_usage(self): - pass - - @abc.abstractmethod - def _get_extended_key_usage(self): - pass - - @abc.abstractmethod - def _get_basic_constraints(self): - pass - - @abc.abstractmethod - def _get_ocsp_must_staple(self): - pass - - @abc.abstractmethod - def _get_subject_alt_name(self): - pass - - @abc.abstractmethod - def _get_public_key(self, binary): - pass - - @abc.abstractmethod - def _get_subject_key_identifier(self): - pass - - @abc.abstractmethod - def _get_authority_key_identifier(self): - pass - - @abc.abstractmethod - def _get_all_extensions(self): - pass - - @abc.abstractmethod - def _is_signature_valid(self): - pass - - def get_info(self): - result = dict() - self.csr = crypto_utils.load_certificate_request(self.path, content=self.content, backend=self.backend) - - subject = self._get_subject_ordered() - result['subject'] = dict() - for k, v in subject: - result['subject'][k] = v - result['subject_ordered'] = subject - result['key_usage'], result['key_usage_critical'] = self._get_key_usage() - result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage() - result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints() - result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple() - result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name() - - result['public_key'] = self._get_public_key(binary=False) - pk = self._get_public_key(binary=True) - result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict() - - if self.backend != 'pyopenssl': - ski = self._get_subject_key_identifier() - if ski is not None: - ski = to_native(binascii.hexlify(ski)) - ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)]) - result['subject_key_identifier'] = ski - - aki, aci, acsn = self._get_authority_key_identifier() - if aki is not None: - aki = to_native(binascii.hexlify(aki)) - aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)]) - result['authority_key_identifier'] = aki - result['authority_cert_issuer'] = aci - result['authority_cert_serial_number'] = acsn - - result['extensions_by_oid'] = self._get_all_extensions() - - result['signature_valid'] = self._is_signature_valid() - if not result['signature_valid']: - self.module.fail_json( - msg='CSR signature is invalid!', - **result - ) - return result - - -class CertificateSigningRequestInfoCryptography(CertificateSigningRequestInfo): - """Validate the supplied CSR, using the cryptography backend""" - def __init__(self, module): - super(CertificateSigningRequestInfoCryptography, self).__init__(module, 'cryptography') - - def _get_subject_ordered(self): - result = [] - for attribute in self.csr.subject: - result.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) - return result - - def _get_key_usage(self): - try: - current_key_ext = self.csr.extensions.get_extension_for_class(x509.KeyUsage) - current_key_usage = current_key_ext.value - key_usage = dict( - digital_signature=current_key_usage.digital_signature, - content_commitment=current_key_usage.content_commitment, - key_encipherment=current_key_usage.key_encipherment, - data_encipherment=current_key_usage.data_encipherment, - key_agreement=current_key_usage.key_agreement, - key_cert_sign=current_key_usage.key_cert_sign, - crl_sign=current_key_usage.crl_sign, - encipher_only=False, - decipher_only=False, - ) - if key_usage['key_agreement']: - key_usage.update(dict( - encipher_only=current_key_usage.encipher_only, - decipher_only=current_key_usage.decipher_only - )) - - key_usage_names = dict( - digital_signature='Digital Signature', - content_commitment='Non Repudiation', - key_encipherment='Key Encipherment', - data_encipherment='Data Encipherment', - key_agreement='Key Agreement', - key_cert_sign='Certificate Sign', - crl_sign='CRL Sign', - encipher_only='Encipher Only', - decipher_only='Decipher Only', - ) - return sorted([ - key_usage_names[name] for name, value in key_usage.items() if value - ]), current_key_ext.critical - except cryptography.x509.ExtensionNotFound: - return None, False - - def _get_extended_key_usage(self): - try: - ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.ExtendedKeyUsage) - return sorted([ - crypto_utils.cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value - ]), ext_keyusage_ext.critical - except cryptography.x509.ExtensionNotFound: - return None, False - - def _get_basic_constraints(self): - try: - ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.BasicConstraints) - result = [] - result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE')) - if ext_keyusage_ext.value.path_length is not None: - result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length)) - return sorted(result), ext_keyusage_ext.critical - except cryptography.x509.ExtensionNotFound: - return None, False - - def _get_ocsp_must_staple(self): - try: - try: - # This only works with cryptography >= 2.1 - tlsfeature_ext = self.csr.extensions.get_extension_for_class(x509.TLSFeature) - value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value - except AttributeError as dummy: - # Fallback for cryptography < 2.1 - oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24") - tlsfeature_ext = self.csr.extensions.get_extension_for_oid(oid) - value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05" - return value, tlsfeature_ext.critical - except cryptography.x509.ExtensionNotFound: - return None, False - - def _get_subject_alt_name(self): - try: - san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName) - result = [crypto_utils.cryptography_decode_name(san) for san in san_ext.value] - return result, san_ext.critical - except cryptography.x509.ExtensionNotFound: - return None, False - - def _get_public_key(self, binary): - return self.csr.public_key().public_bytes( - serialization.Encoding.DER if binary else serialization.Encoding.PEM, - serialization.PublicFormat.SubjectPublicKeyInfo - ) - - def _get_subject_key_identifier(self): - try: - ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier) - return ext.value.digest - except cryptography.x509.ExtensionNotFound: - return None - - def _get_authority_key_identifier(self): - try: - ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) - issuer = None - if ext.value.authority_cert_issuer is not None: - issuer = [crypto_utils.cryptography_decode_name(san) for san in ext.value.authority_cert_issuer] - return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number - except cryptography.x509.ExtensionNotFound: - return None, None, None - - def _get_all_extensions(self): - return crypto_utils.cryptography_get_extensions_from_csr(self.csr) - - def _is_signature_valid(self): - return self.csr.is_signature_valid - - -class CertificateSigningRequestInfoPyOpenSSL(CertificateSigningRequestInfo): - """validate the supplied CSR.""" - - def __init__(self, module): - super(CertificateSigningRequestInfoPyOpenSSL, self).__init__(module, 'pyopenssl') - - def __get_name(self, name): - result = [] - for sub in name.get_components(): - result.append([crypto_utils.pyopenssl_normalize_name(sub[0]), to_text(sub[1])]) - return result - - def _get_subject_ordered(self): - return self.__get_name(self.csr.get_subject()) - - def _get_extension(self, short_name): - for extension in self.csr.get_extensions(): - if extension.get_short_name() == short_name: - result = [ - crypto_utils.pyopenssl_normalize_name(usage.strip()) for usage in to_text(extension, errors='surrogate_or_strict').split(',') - ] - return sorted(result), bool(extension.get_critical()) - return None, False - - def _get_key_usage(self): - return self._get_extension(b'keyUsage') - - def _get_extended_key_usage(self): - return self._get_extension(b'extendedKeyUsage') - - def _get_basic_constraints(self): - return self._get_extension(b'basicConstraints') - - def _get_ocsp_must_staple(self): - extensions = self.csr.get_extensions() - oms_ext = [ - ext for ext in extensions - if to_bytes(ext.get_short_name()) == OPENSSL_MUST_STAPLE_NAME and to_bytes(ext) == OPENSSL_MUST_STAPLE_VALUE - ] - if OpenSSL.SSL.OPENSSL_VERSION_NUMBER < 0x10100000: - # Older versions of libssl don't know about OCSP Must Staple - oms_ext.extend([ext for ext in extensions if ext.get_short_name() == b'UNDEF' and ext.get_data() == b'\x30\x03\x02\x01\x05']) - if oms_ext: - return True, bool(oms_ext[0].get_critical()) - else: - return None, False - - def _normalize_san(self, san): - # apparently openssl returns 'IP address' not 'IP' as specifier when converting the subjectAltName to string - # although it won't accept this specifier when generating the CSR. (https://github.com/openssl/openssl/issues/4004) - if san.startswith('IP Address:'): - san = 'IP:' + san[len('IP Address:'):] - if san.startswith('IP:'): - ip = compat_ipaddress.ip_address(san[3:]) - san = 'IP:{0}'.format(ip.compressed) - return san - - def _get_subject_alt_name(self): - for extension in self.csr.get_extensions(): - if extension.get_short_name() == b'subjectAltName': - result = [self._normalize_san(altname.strip()) for altname in - to_text(extension, errors='surrogate_or_strict').split(', ')] - return result, bool(extension.get_critical()) - return None, False - - def _get_public_key(self, binary): - try: - return crypto.dump_publickey( - crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM, - self.csr.get_pubkey() - ) - except AttributeError: - try: - bio = crypto._new_mem_buf() - if binary: - rc = crypto._lib.i2d_PUBKEY_bio(bio, self.csr.get_pubkey()._pkey) - else: - rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.csr.get_pubkey()._pkey) - if rc != 1: - crypto._raise_current_error() - return crypto._bio_to_string(bio) - except AttributeError: - self.module.warn('Your pyOpenSSL version does not support dumping public keys. ' - 'Please upgrade to version 16.0 or newer, or use the cryptography backend.') - - def _get_subject_key_identifier(self): - # Won't be implemented - return None - - def _get_authority_key_identifier(self): - # Won't be implemented - return None, None, None - - def _get_all_extensions(self): - return crypto_utils.pyopenssl_get_extensions_from_csr(self.csr) - - def _is_signature_valid(self): - try: - return bool(self.csr.verify(self.csr.get_pubkey())) - except crypto.Error: - # OpenSSL error means that key is not consistent - return False - - -def main(): - module = AnsibleModule( - argument_spec=dict( - path=dict(type='path'), - content=dict(type='str'), - select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), - ), - required_one_of=( - ['path', 'content'], - ), - mutually_exclusive=( - ['path', 'content'], - ), - supports_check_mode=True, - ) - - try: - if module.params['path'] is not None: - base_dir = os.path.dirname(module.params['path']) or '.' - if not os.path.isdir(base_dir): - module.fail_json( - name=base_dir, - msg='The directory %s does not exist or the file is not a directory' % base_dir - ) - - backend = module.params['select_crypto_backend'] - if backend == 'auto': - # Detect what backend we can use - can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) - can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) - - # If cryptography is available we'll use it - if can_use_cryptography: - backend = 'cryptography' - elif can_use_pyopenssl: - backend = 'pyopenssl' - - # Fail if no backend has been found - if backend == 'auto': - module.fail_json(msg=("Can't detect any of the required Python libraries " - "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( - MINIMAL_CRYPTOGRAPHY_VERSION, - MINIMAL_PYOPENSSL_VERSION)) - - if backend == 'pyopenssl': - if not PYOPENSSL_FOUND: - module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), - exception=PYOPENSSL_IMP_ERR) - try: - getattr(crypto.X509Req, 'get_extensions') - except AttributeError: - module.fail_json(msg='You need to have PyOpenSSL>=0.15') - - module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') - certificate = CertificateSigningRequestInfoPyOpenSSL(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) - certificate = CertificateSigningRequestInfoCryptography(module) - - result = certificate.get_info() - module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: - module.fail_json(msg=to_native(exc)) - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/crypto/openssl_dhparam.py b/lib/ansible/modules/crypto/openssl_dhparam.py deleted file mode 100644 index 5e06db9717..0000000000 --- a/lib/ansible/modules/crypto/openssl_dhparam.py +++ /dev/null @@ -1,418 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2017, Thom Wiggers <ansible@thomwiggers.nl> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: openssl_dhparam -version_added: "2.5" -short_description: Generate OpenSSL Diffie-Hellman Parameters -description: - - This module allows one to (re)generate OpenSSL DH-params. - - This module uses file common arguments to specify generated file permissions. - - "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: - - Either cryptography >= 2.0 - - Or OpenSSL binary C(openssl) -author: - - Thom Wiggers (@thomwiggers) -options: - state: - description: - - Whether the parameters should exist or not, - taking action if the state is different from what is stated. - type: str - default: present - choices: [ absent, present ] - size: - description: - - Size (in bits) of the generated DH-params. - type: int - default: 4096 - force: - description: - - Should the parameters be regenerated even it it already exists. - type: bool - default: no - path: - description: - - Name of the file in which the generated parameters will be saved. - type: path - required: true - backup: - description: - - Create a backup file including a timestamp so you can get the original - DH params back if you overwrote them with new ones by accident. - 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' - return_content: - description: - - If set to C(yes), will return the (current or generated) DH params' content as I(dhparams). - type: bool - default: no - version_added: "2.10" -extends_documentation_fragment: -- files -seealso: -- module: openssl_certificate -- module: openssl_csr -- module: openssl_pkcs12 -- module: openssl_privatekey -- module: openssl_publickey -''' - -EXAMPLES = r''' -- name: Generate Diffie-Hellman parameters with the default size (4096 bits) - openssl_dhparam: - path: /etc/ssl/dhparams.pem - -- name: Generate DH Parameters with a different size (2048 bits) - openssl_dhparam: - path: /etc/ssl/dhparams.pem - size: 2048 - -- name: Force regenerate an DH parameters if they already exist - openssl_dhparam: - path: /etc/ssl/dhparams.pem - force: yes -''' - -RETURN = r''' -size: - description: Size (in bits) of the Diffie-Hellman parameters. - returned: changed or success - type: int - sample: 4096 -filename: - description: Path to the generated Diffie-Hellman parameters. - returned: changed or success - type: str - sample: /etc/ssl/dhparams.pem -backup_file: - description: Name of backup file created. - returned: changed and if I(backup) is C(yes) - type: str - sample: /path/to/dhparams.pem.2019-03-09@11:22~ -dhparams: - description: The (current or generated) DH params' content. - returned: if I(state) is C(present) and I(return_content) is C(yes) - type: str - version_added: "2.10" -''' - -import abc -import os -import re -import tempfile -import traceback -from distutils.version import LooseVersion - -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 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.return_content = module.params['return_content'] - - 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 DH params.""" - changed = False - - # ony generate when necessary - if self.force or not self._check_params_valid(module): - self._do_generate(module) - changed = True - - # fix permissions (checking force not necessary as done above) - if not self._check_fs_attributes(module): - # Fix done implicitly by - # AnsibleModule.set_fs_attributes_if_different - changed = True - - self.changed = changed - - def remove(self, module): - if self.backup: - self.backup_file = module.backup_local(self.path) - try: - os.remove(self.path) - self.changed = True - except OSError as exc: - module.fail_json(msg=to_native(exc)) - - def check(self, module): - """Ensure the resource is in its desired state.""" - if self.force: - return False - return self._check_params_valid(module) and self._check_fs_attributes(module) - - @abc.abstractmethod - def _check_params_valid(self, module): - """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 - if self.return_content: - content = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - result['dhparams'] = content.decode('utf-8') if content else None - - 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] - rc, out, err = module.run_command(command, check_rc=False) - result = to_native(out) - if rc != 0: - # If the call failed the file probably doesn't exist or is - # unreadable - return False - # output contains "(xxxx bit)" - match = re.search(r"Parameters:\s+\((\d+) bit\).*", result) - if not match: - return False # No "xxxx bit" in output - - bits = int(match.group(1)) - - # if output contains "WARNING" we've got a problem - if "WARNING" in result or "WARNING" in to_native(err): - return False - - return bits == self.size - - -class DHParameterCryptography(DHParameterBase): - - 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) - - 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(): - """Main function""" - - module = AnsibleModule( - argument_spec=dict( - state=dict(type='str', default='present', choices=['absent', 'present']), - size=dict(type='int', default=4096), - 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']), - return_content=dict(type='bool', default=False), - ), - supports_check_mode=True, - add_file_common_args=True, - ) - - base_dir = os.path.dirname(module.params['path']) or '.' - if not os.path.isdir(base_dir): - module.fail_json( - name=base_dir, - msg="The directory '%s' does not exist or the file is not a directory" % base_dir - ) - - 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': - 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() - result['changed'] = module.params['force'] or not dhparam.check(module) - module.exit_json(**result) - - try: - dhparam.generate(module) - except DHParameterError as exc: - module.fail_json(msg=to_native(exc)) - else: - dhparam = DHParameterAbsent(module) - - if module.check_mode: - result = dhparam.dump() - result['changed'] = os.path.exists(module.params['path']) - module.exit_json(**result) - - if os.path.exists(module.params['path']): - try: - dhparam.remove(module) - except Exception as exc: - module.fail_json(msg=to_native(exc)) - - result = dhparam.dump() - - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/openssl_pkcs12.py b/lib/ansible/modules/crypto/openssl_pkcs12.py deleted file mode 100644 index c80e616ef7..0000000000 --- a/lib/ansible/modules/crypto/openssl_pkcs12.py +++ /dev/null @@ -1,470 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2017, Guillaume Delpierre <gde@llew.me> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: openssl_pkcs12 -author: -- Guillaume Delpierre (@gdelpierre) -version_added: "2.7" -short_description: Generate OpenSSL PKCS#12 archive -description: - - This module allows one to (re-)generate PKCS#12. -requirements: - - python-pyOpenSSL -options: - action: - description: - - C(export) or C(parse) a PKCS#12. - type: str - default: export - choices: [ export, parse ] - other_certificates: - description: - - List of other certificates to include. Pre 2.8 this parameter was called C(ca_certificates) - type: list - elements: path - aliases: [ ca_certificates ] - certificate_path: - description: - - The path to read certificates and private keys from. - - Must be in PEM format. - type: path - force: - description: - - Should the file be regenerated even if it already exists. - type: bool - default: no - friendly_name: - description: - - Specifies the friendly name for the certificate and private key. - type: str - aliases: [ name ] - iter_size: - description: - - Number of times to repeat the encryption step. - type: int - default: 2048 - maciter_size: - description: - - Number of times to repeat the MAC step. - type: int - default: 1 - passphrase: - description: - - The PKCS#12 password. - type: str - path: - description: - - Filename to write the PKCS#12 file to. - type: path - required: true - privatekey_passphrase: - description: - - Passphrase source to decrypt any input private keys with. - type: str - privatekey_path: - description: - - File to read private key from. - type: path - state: - description: - - Whether the file should exist or not. - All parameters except C(path) are ignored when state is C(absent). - choices: [ absent, present ] - default: present - type: str - src: - description: - - PKCS#12 file path to parse. - type: path - backup: - description: - - Create a backup file including a timestamp so you can get the original - output file back if you overwrote it with a new one by accident. - type: bool - default: no - version_added: "2.8" - return_content: - description: - - If set to C(yes), will return the (current or generated) PKCS#12's content as I(pkcs12). - type: bool - default: no - version_added: "2.10" -extends_documentation_fragment: - - files -seealso: -- module: openssl_certificate -- module: openssl_csr -- module: openssl_dhparam -- module: openssl_privatekey -- module: openssl_publickey -''' - -EXAMPLES = r''' -- name: Generate PKCS#12 file - openssl_pkcs12: - action: export - path: /opt/certs/ansible.p12 - friendly_name: raclette - privatekey_path: /opt/certs/keys/key.pem - certificate_path: /opt/certs/cert.pem - other_certificates: /opt/certs/ca.pem - state: present - -- name: Change PKCS#12 file permission - openssl_pkcs12: - action: export - path: /opt/certs/ansible.p12 - friendly_name: raclette - privatekey_path: /opt/certs/keys/key.pem - certificate_path: /opt/certs/cert.pem - other_certificates: /opt/certs/ca.pem - state: present - mode: '0600' - -- name: Regen PKCS#12 file - openssl_pkcs12: - action: export - src: /opt/certs/ansible.p12 - path: /opt/certs/ansible.p12 - friendly_name: raclette - privatekey_path: /opt/certs/keys/key.pem - certificate_path: /opt/certs/cert.pem - other_certificates: /opt/certs/ca.pem - state: present - mode: '0600' - force: yes - -- name: Dump/Parse PKCS#12 file - openssl_pkcs12: - action: parse - src: /opt/certs/ansible.p12 - path: /opt/certs/ansible.pem - state: present - -- name: Remove PKCS#12 file - openssl_pkcs12: - path: /opt/certs/ansible.p12 - state: absent -''' - -RETURN = r''' -filename: - description: Path to the generate PKCS#12 file. - returned: changed or success - type: str - sample: /opt/certs/ansible.p12 -privatekey: - description: Path to the TLS/SSL private key the public key was generated from. - returned: changed or success - type: str - sample: /etc/ssl/private/ansible.com.pem -backup_file: - description: Name of backup file created. - returned: changed and if I(backup) is C(yes) - type: str - sample: /path/to/ansible.com.pem.2019-03-09@11:22~ -pkcs12: - description: The (current or generated) PKCS#12's content Base64 encoded. - returned: if I(state) is C(present) and I(return_content) is C(yes) - type: str - version_added: "2.10" -''' - -import base64 -import stat -import os -import traceback - -PYOPENSSL_IMP_ERR = None -try: - from OpenSSL import crypto -except ImportError: - PYOPENSSL_IMP_ERR = traceback.format_exc() - pyopenssl_found = False -else: - pyopenssl_found = True - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils import crypto as crypto_utils -from ansible.module_utils._text import to_bytes, to_native - - -class PkcsError(crypto_utils.OpenSSLObjectError): - pass - - -class Pkcs(crypto_utils.OpenSSLObject): - - def __init__(self, module): - super(Pkcs, self).__init__( - module.params['path'], - module.params['state'], - module.params['force'], - module.check_mode - ) - self.action = module.params['action'] - self.other_certificates = module.params['other_certificates'] - self.certificate_path = module.params['certificate_path'] - self.friendly_name = module.params['friendly_name'] - self.iter_size = module.params['iter_size'] - self.maciter_size = module.params['maciter_size'] - self.passphrase = module.params['passphrase'] - self.pkcs12 = None - self.privatekey_passphrase = module.params['privatekey_passphrase'] - self.privatekey_path = module.params['privatekey_path'] - self.pkcs12_bytes = None - self.return_content = module.params['return_content'] - self.src = module.params['src'] - - if module.params['mode'] is None: - module.params['mode'] = '0400' - - self.backup = module.params['backup'] - self.backup_file = None - - def check(self, module, perms_required=True): - """Ensure the resource is in its desired state.""" - - state_and_perms = super(Pkcs, self).check(module, perms_required) - - def _check_pkey_passphrase(): - if self.privatekey_passphrase: - try: - crypto_utils.load_privatekey(self.privatekey_path, - self.privatekey_passphrase) - except crypto.Error: - return False - except crypto_utils.OpenSSLBadPassphraseError: - return False - return True - - if not state_and_perms: - return state_and_perms - - if os.path.exists(self.path) and module.params['action'] == 'export': - dummy = self.generate(module) - self.src = self.path - try: - pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse() - except crypto.Error: - return False - if (pkcs12_privatekey is not None) and (self.privatekey_path is not None): - expected_pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, - self.pkcs12.get_privatekey()) - if pkcs12_privatekey != expected_pkey: - return False - elif bool(pkcs12_privatekey) != bool(self.privatekey_path): - return False - - if (pkcs12_certificate is not None) and (self.certificate_path is not None): - - expected_cert = crypto.dump_certificate(crypto.FILETYPE_PEM, - self.pkcs12.get_certificate()) - if pkcs12_certificate != expected_cert: - return False - elif bool(pkcs12_certificate) != bool(self.certificate_path): - return False - - if (pkcs12_other_certificates is not None) and (self.other_certificates is not None): - expected_other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM, - other_cert) for other_cert in self.pkcs12.get_ca_certificates()] - if set(pkcs12_other_certificates) != set(expected_other_certs): - return False - elif bool(pkcs12_other_certificates) != bool(self.other_certificates): - return False - - if pkcs12_privatekey: - # This check is required because pyOpenSSL will not return a friendly name - # if the private key is not set in the file - if ((self.pkcs12.get_friendlyname() is not None) and (pkcs12_friendly_name is not None)): - if self.pkcs12.get_friendlyname() != pkcs12_friendly_name: - return False - elif bool(self.pkcs12.get_friendlyname()) != bool(pkcs12_friendly_name): - return False - else: - return False - - return _check_pkey_passphrase() - - def dump(self): - """Serialize the object into a dictionary.""" - - result = { - 'filename': self.path, - } - if self.privatekey_path: - result['privatekey_path'] = self.privatekey_path - if self.backup_file: - result['backup_file'] = self.backup_file - if self.return_content: - if self.pkcs12_bytes is None: - self.pkcs12_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - result['pkcs12'] = base64.b64encode(self.pkcs12_bytes) if self.pkcs12_bytes else None - - return result - - def generate(self, module): - """Generate PKCS#12 file archive.""" - self.pkcs12 = crypto.PKCS12() - - if self.other_certificates: - other_certs = [crypto_utils.load_certificate(other_cert) for other_cert - in self.other_certificates] - self.pkcs12.set_ca_certificates(other_certs) - - if self.certificate_path: - self.pkcs12.set_certificate(crypto_utils.load_certificate( - self.certificate_path)) - - if self.friendly_name: - self.pkcs12.set_friendlyname(to_bytes(self.friendly_name)) - - if self.privatekey_path: - try: - self.pkcs12.set_privatekey(crypto_utils.load_privatekey( - self.privatekey_path, - self.privatekey_passphrase) - ) - except crypto_utils.OpenSSLBadPassphraseError as exc: - raise PkcsError(exc) - - return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size) - - def remove(self, module): - if self.backup: - self.backup_file = module.backup_local(self.path) - super(Pkcs, self).remove(module) - - def parse(self): - """Read PKCS#12 file.""" - - try: - with open(self.src, 'rb') as pkcs12_fh: - pkcs12_content = pkcs12_fh.read() - p12 = crypto.load_pkcs12(pkcs12_content, - self.passphrase) - pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, - p12.get_privatekey()) - crt = crypto.dump_certificate(crypto.FILETYPE_PEM, - p12.get_certificate()) - other_certs = [] - if p12.get_ca_certificates() is not None: - other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM, - other_cert) for other_cert in p12.get_ca_certificates()] - - friendly_name = p12.get_friendlyname() - - return (pkey, crt, other_certs, friendly_name) - - except IOError as exc: - raise PkcsError(exc) - - def write(self, module, content, mode=None): - """Write the PKCS#12 file.""" - if self.backup: - self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, content, mode) - if self.return_content: - self.pkcs12_bytes = content - - -def main(): - argument_spec = dict( - action=dict(type='str', default='export', choices=['export', 'parse']), - other_certificates=dict(type='list', elements='path', aliases=['ca_certificates']), - certificate_path=dict(type='path'), - force=dict(type='bool', default=False), - friendly_name=dict(type='str', aliases=['name']), - iter_size=dict(type='int', default=2048), - maciter_size=dict(type='int', default=1), - passphrase=dict(type='str', no_log=True), - path=dict(type='path', required=True), - privatekey_passphrase=dict(type='str', no_log=True), - privatekey_path=dict(type='path'), - state=dict(type='str', default='present', choices=['absent', 'present']), - src=dict(type='path'), - backup=dict(type='bool', default=False), - return_content=dict(type='bool', default=False), - ) - - required_if = [ - ['action', 'parse', ['src']], - ] - - module = AnsibleModule( - add_file_common_args=True, - argument_spec=argument_spec, - required_if=required_if, - supports_check_mode=True, - ) - - if not pyopenssl_found: - module.fail_json(msg=missing_required_lib('pyOpenSSL'), exception=PYOPENSSL_IMP_ERR) - - base_dir = os.path.dirname(module.params['path']) or '.' - if not os.path.isdir(base_dir): - module.fail_json( - name=base_dir, - msg="The directory '%s' does not exist or the path is not a directory" % base_dir - ) - - try: - pkcs12 = Pkcs(module) - changed = False - - if module.params['state'] == 'present': - if module.check_mode: - result = pkcs12.dump() - result['changed'] = module.params['force'] or not pkcs12.check(module) - module.exit_json(**result) - - if not pkcs12.check(module, perms_required=False) or module.params['force']: - if module.params['action'] == 'export': - if not module.params['friendly_name']: - module.fail_json(msg='Friendly_name is required') - pkcs12_content = pkcs12.generate(module) - pkcs12.write(module, pkcs12_content, 0o600) - changed = True - else: - pkey, cert, other_certs, friendly_name = pkcs12.parse() - dump_content = '%s%s%s' % (to_native(pkey), to_native(cert), to_native(b''.join(other_certs))) - pkcs12.write(module, to_bytes(dump_content)) - - file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, changed): - changed = True - else: - if module.check_mode: - result = pkcs12.dump() - result['changed'] = os.path.exists(module.params['path']) - module.exit_json(**result) - - if os.path.exists(module.params['path']): - pkcs12.remove(module) - changed = True - - result = pkcs12.dump() - result['changed'] = changed - if os.path.exists(module.params['path']): - file_mode = "%04o" % stat.S_IMODE(os.stat(module.params['path']).st_mode) - result['mode'] = file_mode - - module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: - module.fail_json(msg=to_native(exc)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/openssl_privatekey.py b/lib/ansible/modules/crypto/openssl_privatekey.py deleted file mode 100644 index 2fdfdab10c..0000000000 --- a/lib/ansible/modules/crypto/openssl_privatekey.py +++ /dev/null @@ -1,943 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2016, Yanis Guenane <yanis+ansible@guenane.org> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: openssl_privatekey -version_added: "2.3" -short_description: Generate OpenSSL private keys -description: - - This module allows one to (re)generate OpenSSL private keys. - - One can generate L(RSA,https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29), - L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm), - L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) or - L(EdDSA,https://en.wikipedia.org/wiki/EdDSA) private keys. - - Keys are generated in PEM format. - - "Please note that the module regenerates private keys if they don't match - the module's options. In particular, if you provide another passphrase - (or specify none), change the keysize, etc., the private key will be - regenerated. If you are concerned that this could **overwrite your private key**, - consider using the I(backup) option." - - The module can use the cryptography Python library, or the pyOpenSSL Python - library. By default, it tries to detect which one is available. This can be - overridden with the I(select_crypto_backend) option. Please note that the - PyOpenSSL backend was deprecated in Ansible 2.9 and will be removed in Ansible 2.13." -requirements: - - Either cryptography >= 1.2.3 (older versions might work as well) - - Or pyOpenSSL -author: - - Yanis Guenane (@Spredzy) - - Felix Fontein (@felixfontein) -options: - state: - description: - - Whether the private key should exist or not, taking action if the state is different from what is stated. - type: str - default: present - choices: [ absent, present ] - size: - description: - - Size (in bits) of the TLS/SSL key to generate. - type: int - default: 4096 - type: - description: - - The algorithm used to generate the TLS/SSL private key. - - Note that C(ECC), C(X25519), C(X448), C(Ed25519) and C(Ed448) require the C(cryptography) backend. - C(X25519) needs cryptography 2.5 or newer, while C(X448), C(Ed25519) and C(Ed448) require - cryptography 2.6 or newer. For C(ECC), the minimal cryptography version required depends on the - I(curve) option. - type: str - default: RSA - choices: [ DSA, ECC, Ed25519, Ed448, RSA, X25519, X448 ] - curve: - description: - - Note that not all curves are supported by all versions of C(cryptography). - - For maximal interoperability, C(secp384r1) or C(secp256r1) should be used. - - We use the curve names as defined in the - L(IANA registry for TLS,https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8). - type: str - choices: - - secp384r1 - - secp521r1 - - secp224r1 - - secp192r1 - - secp256r1 - - secp256k1 - - brainpoolP256r1 - - brainpoolP384r1 - - brainpoolP512r1 - - sect571k1 - - sect409k1 - - sect283k1 - - sect233k1 - - sect163k1 - - sect571r1 - - sect409r1 - - sect283r1 - - sect233r1 - - sect163r2 - version_added: "2.8" - force: - description: - - Should the key be regenerated even if it already exists. - type: bool - default: no - path: - description: - - Name of the file in which the generated TLS/SSL private key will be written. It will have 0600 mode. - type: path - required: true - passphrase: - description: - - The passphrase for the private key. - type: str - version_added: "2.4" - cipher: - description: - - The cipher to encrypt the private key. (Valid values can be found by - running `openssl list -cipher-algorithms` or `openssl list-cipher-algorithms`, - depending on your OpenSSL version.) - - When using the C(cryptography) backend, use C(auto). - type: str - version_added: "2.4" - 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(pyopenssl). - - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. - - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. - - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. - From that point on, only the C(cryptography) backend will be available. - type: str - default: auto - choices: [ auto, cryptography, pyopenssl ] - version_added: "2.8" - format: - description: - - Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format) - is used for all keys which support it. Please note that not every key can be exported in any format. - - The value C(auto) selects a fromat based on the key format. The value C(auto_ignore) does the same, - but for existing private key files, it will not force a regenerate when its format is not the automatically - selected one for generation. - - Note that if the format for an existing private key mismatches, the key is *regenerated* by default. - To change this behavior, use the I(format_mismatch) option. - - The I(format) option is only supported by the C(cryptography) backend. The C(pyopenssl) backend will - fail if a value different from C(auto_ignore) is used. - type: str - default: auto_ignore - choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ] - version_added: "2.10" - format_mismatch: - description: - - Determines behavior of the module if the format of a private key does not match the expected format, but all - other parameters are as expected. - - If set to C(regenerate) (default), generates a new private key. - - If set to C(convert), the key will be converted to the new format instead. - - Only supported by the C(cryptography) backend. - type: str - default: regenerate - choices: [ regenerate, convert ] - version_added: "2.10" - backup: - description: - - Create a backup file including a timestamp so you can get - the original private key back if you overwrote it with a new one by accident. - type: bool - default: no - version_added: "2.8" - return_content: - description: - - If set to C(yes), will return the (current or generated) private key's content as I(privatekey). - - Note that especially if the private key is not encrypted, you have to make sure that the returned - value is treated appropriately and not accidentally written to logs etc.! Use with care! - type: bool - default: no - version_added: "2.10" - regenerate: - description: - - Allows to configure in which situations the module is allowed to regenerate private keys. - The module will always generate a new key if the destination file does not exist. - - By default, the key will be regenerated when it doesn't match the module's options, - except when the key cannot be read or the passphrase does not match. Please note that - this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence) - is specified. - - If set to C(never), the module will fail if the key cannot be read or the passphrase - isn't matching, and will never regenerate an existing key. - - If set to C(fail), the module will fail if the key does not correspond to the module's - options. - - If set to C(partial_idempotence), the key will be regenerated if it does not conform to - the module's options. The key is B(not) regenerated if it cannot be read (broken file), - the key is protected by an unknown passphrase, or when they key is not protected by a - passphrase, but a passphrase is specified. - - If set to C(full_idempotence), the key will be regenerated if it does not conform to the - module's options. This is also the case if the key cannot be read (broken file), the key - is protected by an unknown passphrase, or when they key is not protected by a passphrase, - but a passphrase is specified. Make sure you have a B(backup) when using this option! - - If set to C(always), the module will always regenerate the key. This is equivalent to - setting I(force) to C(yes). - - Note that if I(format_mismatch) is set to C(convert) and everything matches except the - format, the key will always be converted, except if I(regenerate) is set to C(always). - type: str - choices: - - never - - fail - - partial_idempotence - - full_idempotence - - always - default: full_idempotence - version_added: '2.10' -extends_documentation_fragment: -- files -seealso: -- module: openssl_certificate -- module: openssl_csr -- module: openssl_dhparam -- module: openssl_pkcs12 -- module: openssl_publickey -''' - -EXAMPLES = r''' -- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) - openssl_privatekey: - path: /etc/ssl/private/ansible.com.pem - -- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) and a passphrase - openssl_privatekey: - path: /etc/ssl/private/ansible.com.pem - passphrase: ansible - cipher: aes256 - -- name: Generate an OpenSSL private key with a different size (2048 bits) - openssl_privatekey: - path: /etc/ssl/private/ansible.com.pem - size: 2048 - -- name: Force regenerate an OpenSSL private key if it already exists - openssl_privatekey: - path: /etc/ssl/private/ansible.com.pem - force: yes - -- name: Generate an OpenSSL private key with a different algorithm (DSA) - openssl_privatekey: - path: /etc/ssl/private/ansible.com.pem - type: DSA -''' - -RETURN = r''' -size: - description: Size (in bits) of the TLS/SSL private key. - returned: changed or success - type: int - sample: 4096 -type: - description: Algorithm used to generate the TLS/SSL private key. - returned: changed or success - type: str - sample: RSA -curve: - description: Elliptic curve used to generate the TLS/SSL private key. - returned: changed or success, and I(type) is C(ECC) - type: str - sample: secp256r1 -filename: - description: Path to the generated TLS/SSL private key file. - returned: changed or success - type: str - sample: /etc/ssl/private/ansible.com.pem -fingerprint: - description: - - The fingerprint of the public key. Fingerprint will be generated for each C(hashlib.algorithms) available. - - The PyOpenSSL backend requires PyOpenSSL >= 16.0 for meaningful output. - returned: changed or success - type: dict - sample: - md5: "84:75:71:72:8d:04:b5:6c:4d:37:6d:66:83:f5:4c:29" - sha1: "51:cc:7c:68:5d:eb:41:43:88:7e:1a:ae:c7:f8:24:72:ee:71:f6:10" - sha224: "b1:19:a6:6c:14:ac:33:1d:ed:18:50:d3:06:5c:b2:32:91:f1:f1:52:8c:cb:d5:75:e9:f5:9b:46" - sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7" - sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d" - sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b" -backup_file: - description: Name of backup file created. - returned: changed and if I(backup) is C(yes) - type: str - sample: /path/to/privatekey.pem.2019-03-09@11:22~ -privatekey: - description: - - The (current or generated) private key's content. - - Will be Base64-encoded if the key is in raw format. - returned: if I(state) is C(present) and I(return_content) is C(yes) - type: str - version_added: "2.10" -''' - -import abc -import base64 -import os -import traceback -from distutils.version import LooseVersion - -MINIMAL_PYOPENSSL_VERSION = '0.6' -MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' - -PYOPENSSL_IMP_ERR = None -try: - import OpenSSL - from OpenSSL import crypto - PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) -except ImportError: - PYOPENSSL_IMP_ERR = traceback.format_exc() - PYOPENSSL_FOUND = False -else: - PYOPENSSL_FOUND = True - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - import cryptography.exceptions - import cryptography.hazmat.backends - import cryptography.hazmat.primitives.serialization - import cryptography.hazmat.primitives.asymmetric.rsa - import cryptography.hazmat.primitives.asymmetric.dsa - import cryptography.hazmat.primitives.asymmetric.ec - import cryptography.hazmat.primitives.asymmetric.utils - CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - -from ansible.module_utils.crypto import ( - CRYPTOGRAPHY_HAS_X25519, - CRYPTOGRAPHY_HAS_X25519_FULL, - CRYPTOGRAPHY_HAS_X448, - CRYPTOGRAPHY_HAS_ED25519, - CRYPTOGRAPHY_HAS_ED448, -) - -from ansible.module_utils import crypto as crypto_utils -from ansible.module_utils._text import to_native, to_bytes -from ansible.module_utils.basic import AnsibleModule, missing_required_lib - - -class PrivateKeyError(crypto_utils.OpenSSLObjectError): - pass - - -class PrivateKeyBase(crypto_utils.OpenSSLObject): - - def __init__(self, module): - super(PrivateKeyBase, self).__init__( - module.params['path'], - module.params['state'], - module.params['force'], - module.check_mode - ) - self.size = module.params['size'] - self.passphrase = module.params['passphrase'] - self.cipher = module.params['cipher'] - self.privatekey = None - self.fingerprint = {} - self.format = module.params['format'] - self.format_mismatch = module.params['format_mismatch'] - self.privatekey_bytes = None - self.return_content = module.params['return_content'] - self.regenerate = module.params['regenerate'] - if self.regenerate == 'always': - self.force = True - - self.backup = module.params['backup'] - self.backup_file = None - - if module.params['mode'] is None: - module.params['mode'] = '0600' - - @abc.abstractmethod - def _generate_private_key(self): - """(Re-)Generate private key.""" - pass - - @abc.abstractmethod - def _ensure_private_key_loaded(self): - """Make sure that the private key has been loaded.""" - pass - - @abc.abstractmethod - def _get_private_key_data(self): - """Return bytes for self.privatekey""" - pass - - @abc.abstractmethod - def _get_fingerprint(self): - pass - - def generate(self, module): - """Generate a keypair.""" - - if not self.check(module, perms_required=False, ignore_conversion=True) or self.force: - # Regenerate - if self.backup: - self.backup_file = module.backup_local(self.path) - self._generate_private_key() - privatekey_data = self._get_private_key_data() - if self.return_content: - self.privatekey_bytes = privatekey_data - crypto_utils.write_file(module, privatekey_data, 0o600) - self.changed = True - elif not self.check(module, perms_required=False, ignore_conversion=False): - # Convert - if self.backup: - self.backup_file = module.backup_local(self.path) - self._ensure_private_key_loaded() - privatekey_data = self._get_private_key_data() - if self.return_content: - self.privatekey_bytes = privatekey_data - crypto_utils.write_file(module, privatekey_data, 0o600) - self.changed = True - - self.fingerprint = self._get_fingerprint() - 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, module): - if self.backup: - self.backup_file = module.backup_local(self.path) - super(PrivateKeyBase, self).remove(module) - - @abc.abstractmethod - def _check_passphrase(self): - pass - - @abc.abstractmethod - def _check_size_and_type(self): - pass - - @abc.abstractmethod - def _check_format(self): - pass - - def check(self, module, perms_required=True, ignore_conversion=True): - """Ensure the resource is in its desired state.""" - - state_and_perms = super(PrivateKeyBase, self).check(module, perms_required=False) - - if not state_and_perms: - # key does not exist - return False - - if not self._check_passphrase(): - if self.regenerate in ('full_idempotence', 'always'): - return False - module.fail_json(msg='Unable to read the key. The key is protected with a another passphrase / no passphrase or broken.' - ' Will not proceed. To force regeneration, call the module with `generate`' - ' set to `full_idempotence` or `always`, or with `force=yes`.') - - if self.regenerate != 'never': - if not self._check_size_and_type(): - if self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'): - return False - module.fail_json(msg='Key has wrong type and/or size.' - ' Will not proceed. To force regeneration, call the module with `generate`' - ' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.') - - if not self._check_format(): - # During conversion step, convert if format does not match and format_mismatch == 'convert' - if not ignore_conversion and self.format_mismatch == 'convert': - return False - # During generation step, regenerate if format does not match and format_mismatch == 'regenerate' - if ignore_conversion and self.format_mismatch == 'regenerate' and self.regenerate != 'never': - if not ignore_conversion or self.regenerate in ('partial_idempotence', 'full_idempotence', 'always'): - return False - module.fail_json(msg='Key has wrong format.' - ' Will not proceed. To force regeneration, call the module with `generate`' - ' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=yes`.' - ' To convert the key, set `format_mismatch` to `convert`.') - - # check whether permissions are correct (in case that needs to be checked) - return not perms_required or super(PrivateKeyBase, self).check(module, perms_required=perms_required) - - def dump(self): - """Serialize the object into a dictionary.""" - - result = { - 'size': self.size, - 'filename': self.path, - 'changed': self.changed, - 'fingerprint': self.fingerprint, - } - if self.backup_file: - result['backup_file'] = self.backup_file - if self.return_content: - if self.privatekey_bytes is None: - self.privatekey_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - if self.privatekey_bytes: - if crypto_utils.identify_private_key_format(self.privatekey_bytes) == 'raw': - result['privatekey'] = base64.b64encode(self.privatekey_bytes) - else: - result['privatekey'] = self.privatekey_bytes.decode('utf-8') - else: - result['privatekey'] = None - - return result - - -# Implementation with using pyOpenSSL -class PrivateKeyPyOpenSSL(PrivateKeyBase): - - def __init__(self, module): - super(PrivateKeyPyOpenSSL, self).__init__(module) - - if module.params['type'] == 'RSA': - self.type = crypto.TYPE_RSA - elif module.params['type'] == 'DSA': - self.type = crypto.TYPE_DSA - else: - module.fail_json(msg="PyOpenSSL backend only supports RSA and DSA keys.") - - if self.format != 'auto_ignore': - module.fail_json(msg="PyOpenSSL backend only supports auto_ignore format.") - - def _generate_private_key(self): - """(Re-)Generate private key.""" - self.privatekey = crypto.PKey() - try: - self.privatekey.generate_key(self.type, self.size) - except (TypeError, ValueError) as exc: - raise PrivateKeyError(exc) - - def _ensure_private_key_loaded(self): - """Make sure that the private key has been loaded.""" - if self.privatekey is None: - try: - self.privatekey = privatekey = crypto_utils.load_privatekey(self.path, self.passphrase) - except crypto_utils.OpenSSLBadPassphraseError as exc: - raise PrivateKeyError(exc) - - def _get_private_key_data(self): - """Return bytes for self.privatekey""" - if self.cipher and self.passphrase: - return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey, - self.cipher, to_bytes(self.passphrase)) - else: - return crypto.dump_privatekey(crypto.FILETYPE_PEM, self.privatekey) - - def _get_fingerprint(self): - return crypto_utils.get_fingerprint(self.path, self.passphrase) - - def _check_passphrase(self): - try: - crypto_utils.load_privatekey(self.path, self.passphrase) - return True - except Exception as dummy: - return False - - def _check_size_and_type(self): - def _check_size(privatekey): - return self.size == privatekey.bits() - - def _check_type(privatekey): - return self.type == privatekey.type() - - self._ensure_private_key_loaded() - return _check_size(self.privatekey) and _check_type(self.privatekey) - - def _check_format(self): - # Not supported by this backend - return True - - def dump(self): - """Serialize the object into a dictionary.""" - - result = super(PrivateKeyPyOpenSSL, self).dump() - - if self.type == crypto.TYPE_RSA: - result['type'] = 'RSA' - else: - result['type'] = 'DSA' - - return result - - -# Implementation with using cryptography -class PrivateKeyCryptography(PrivateKeyBase): - - def _get_ec_class(self, ectype): - ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(ectype) - if ecclass is None: - self.module.fail_json(msg='Your cryptography version does not support {0}'.format(ectype)) - return ecclass - - def _add_curve(self, name, ectype, deprecated=False): - def create(size): - ecclass = self._get_ec_class(ectype) - return ecclass() - - def verify(privatekey): - ecclass = self._get_ec_class(ectype) - return isinstance(privatekey.private_numbers().public_numbers.curve, ecclass) - - self.curves[name] = { - 'create': create, - 'verify': verify, - 'deprecated': deprecated, - } - - def __init__(self, module): - super(PrivateKeyCryptography, self).__init__(module) - - self.curves = dict() - self._add_curve('secp384r1', 'SECP384R1') - self._add_curve('secp521r1', 'SECP521R1') - self._add_curve('secp224r1', 'SECP224R1') - self._add_curve('secp192r1', 'SECP192R1') - self._add_curve('secp256r1', 'SECP256R1') - self._add_curve('secp256k1', 'SECP256K1') - self._add_curve('brainpoolP256r1', 'BrainpoolP256R1', deprecated=True) - self._add_curve('brainpoolP384r1', 'BrainpoolP384R1', deprecated=True) - self._add_curve('brainpoolP512r1', 'BrainpoolP512R1', deprecated=True) - self._add_curve('sect571k1', 'SECT571K1', deprecated=True) - self._add_curve('sect409k1', 'SECT409K1', deprecated=True) - self._add_curve('sect283k1', 'SECT283K1', deprecated=True) - self._add_curve('sect233k1', 'SECT233K1', deprecated=True) - self._add_curve('sect163k1', 'SECT163K1', deprecated=True) - self._add_curve('sect571r1', 'SECT571R1', deprecated=True) - self._add_curve('sect409r1', 'SECT409R1', deprecated=True) - self._add_curve('sect283r1', 'SECT283R1', deprecated=True) - self._add_curve('sect233r1', 'SECT233R1', deprecated=True) - self._add_curve('sect163r2', 'SECT163R2', deprecated=True) - - self.module = module - self.cryptography_backend = cryptography.hazmat.backends.default_backend() - - self.type = module.params['type'] - self.curve = module.params['curve'] - if not CRYPTOGRAPHY_HAS_X25519 and self.type == 'X25519': - self.module.fail_json(msg='Your cryptography version does not support X25519') - if not CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519': - self.module.fail_json(msg='Your cryptography version does not support X25519 serialization') - if not CRYPTOGRAPHY_HAS_X448 and self.type == 'X448': - self.module.fail_json(msg='Your cryptography version does not support X448') - if not CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519': - self.module.fail_json(msg='Your cryptography version does not support Ed25519') - if not CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448': - self.module.fail_json(msg='Your cryptography version does not support Ed448') - - def _get_wanted_format(self): - if self.format not in ('auto', 'auto_ignore'): - return self.format - if self.type in ('X25519', 'X448', 'Ed25519', 'Ed448'): - return 'pkcs8' - else: - return 'pkcs1' - - def _generate_private_key(self): - """(Re-)Generate private key.""" - try: - if self.type == 'RSA': - self.privatekey = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key( - public_exponent=65537, # OpenSSL always uses this - key_size=self.size, - backend=self.cryptography_backend - ) - if self.type == 'DSA': - self.privatekey = cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key( - key_size=self.size, - backend=self.cryptography_backend - ) - if CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519': - self.privatekey = cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate() - if CRYPTOGRAPHY_HAS_X448 and self.type == 'X448': - self.privatekey = cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate() - if CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519': - self.privatekey = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate() - if CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448': - self.privatekey = cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.generate() - if self.type == 'ECC' and self.curve in self.curves: - if self.curves[self.curve]['deprecated']: - self.module.warn('Elliptic curves of type {0} should not be used for new keys!'.format(self.curve)) - self.privatekey = cryptography.hazmat.primitives.asymmetric.ec.generate_private_key( - curve=self.curves[self.curve]['create'](self.size), - backend=self.cryptography_backend - ) - except cryptography.exceptions.UnsupportedAlgorithm as dummy: - self.module.fail_json(msg='Cryptography backend does not support the algorithm required for {0}'.format(self.type)) - - def _ensure_private_key_loaded(self): - """Make sure that the private key has been loaded.""" - if self.privatekey is None: - self.privatekey = self._load_privatekey() - - def _get_private_key_data(self): - """Return bytes for self.privatekey""" - # Select export format and encoding - try: - export_format = self._get_wanted_format() - export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM - if export_format == 'pkcs1': - # "TraditionalOpenSSL" format is PKCS1 - export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL - elif export_format == 'pkcs8': - export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8 - elif export_format == 'raw': - export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.Raw - export_encoding = cryptography.hazmat.primitives.serialization.Encoding.Raw - except AttributeError: - self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format)) - - # Select key encryption - encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption() - if self.cipher and self.passphrase: - if self.cipher == 'auto': - encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.passphrase)) - else: - self.module.fail_json(msg='Cryptography backend can only use "auto" for cipher option.') - - # Serialize key - try: - return self.privatekey.private_bytes( - encoding=export_encoding, - format=export_format, - encryption_algorithm=encryption_algorithm - ) - except ValueError as dummy: - self.module.fail_json( - msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format) - ) - except Exception as dummy: - self.module.fail_json( - msg='Error while serializing the private key in the required format "{0}"'.format(self.format), - exception=traceback.format_exc() - ) - - def _load_privatekey(self): - try: - # Read bytes - with open(self.path, 'rb') as f: - data = f.read() - # Interpret bytes depending on format. - format = crypto_utils.identify_private_key_format(data) - if format == 'raw': - if len(data) == 56 and CRYPTOGRAPHY_HAS_X448: - return cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data) - if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448: - return cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data) - if len(data) == 32: - if CRYPTOGRAPHY_HAS_X25519 and (self.type == 'X25519' or not CRYPTOGRAPHY_HAS_ED25519): - return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) - if CRYPTOGRAPHY_HAS_ED25519 and (self.type == 'Ed25519' or not CRYPTOGRAPHY_HAS_X25519): - return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) - if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519: - try: - return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data) - except Exception: - return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data) - raise PrivateKeyError('Cannot load raw key') - else: - return cryptography.hazmat.primitives.serialization.load_pem_private_key( - data, - None if self.passphrase is None else to_bytes(self.passphrase), - backend=self.cryptography_backend - ) - except Exception as e: - raise PrivateKeyError(e) - - def _get_fingerprint(self): - # Get bytes of public key - private_key = self._load_privatekey() - public_key = private_key.public_key() - public_key_bytes = public_key.public_bytes( - cryptography.hazmat.primitives.serialization.Encoding.DER, - cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo - ) - # Get fingerprints of public_key_bytes - return crypto_utils.get_fingerprint_of_bytes(public_key_bytes) - - def _check_passphrase(self): - try: - with open(self.path, 'rb') as f: - data = f.read() - format = crypto_utils.identify_private_key_format(data) - if format == 'raw': - # Raw keys cannot be encrypted. To avoid incompatibilities, we try to - # actually load the key (and return False when this fails). - self._load_privatekey() - # Loading the key succeeded. Only return True when no passphrase was - # provided. - return self.passphrase is None - else: - return cryptography.hazmat.primitives.serialization.load_pem_private_key( - data, - None if self.passphrase is None else to_bytes(self.passphrase), - backend=self.cryptography_backend - ) - except Exception as dummy: - return False - - def _check_size_and_type(self): - self._ensure_private_key_loaded() - - if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): - return self.type == 'RSA' and self.size == self.privatekey.key_size - if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): - return self.type == 'DSA' and self.size == self.privatekey.key_size - if CRYPTOGRAPHY_HAS_X25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey): - return self.type == 'X25519' - if CRYPTOGRAPHY_HAS_X448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey): - return self.type == 'X448' - if CRYPTOGRAPHY_HAS_ED25519 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): - return self.type == 'Ed25519' - if CRYPTOGRAPHY_HAS_ED448 and isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): - return self.type == 'Ed448' - if isinstance(self.privatekey, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): - if self.type != 'ECC': - return False - if self.curve not in self.curves: - return False - return self.curves[self.curve]['verify'](self.privatekey) - - return False - - def _check_format(self): - if self.format == 'auto_ignore': - return True - try: - with open(self.path, 'rb') as f: - content = f.read() - format = crypto_utils.identify_private_key_format(content) - return format == self._get_wanted_format() - except Exception as dummy: - return False - - def dump(self): - """Serialize the object into a dictionary.""" - result = super(PrivateKeyCryptography, self).dump() - result['type'] = self.type - if self.type == 'ECC': - result['curve'] = self.curve - return result - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - state=dict(type='str', default='present', choices=['present', 'absent']), - size=dict(type='int', default=4096), - type=dict(type='str', default='RSA', choices=[ - 'DSA', 'ECC', 'Ed25519', 'Ed448', 'RSA', 'X25519', 'X448' - ]), - curve=dict(type='str', choices=[ - 'secp384r1', 'secp521r1', 'secp224r1', 'secp192r1', 'secp256r1', - 'secp256k1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1', - 'sect571k1', 'sect409k1', 'sect283k1', 'sect233k1', 'sect163k1', - 'sect571r1', 'sect409r1', 'sect283r1', 'sect233r1', 'sect163r2', - ]), - force=dict(type='bool', default=False), - path=dict(type='path', required=True), - passphrase=dict(type='str', no_log=True), - cipher=dict(type='str'), - backup=dict(type='bool', default=False), - format=dict(type='str', default='auto_ignore', choices=['pkcs1', 'pkcs8', 'raw', 'auto', 'auto_ignore']), - format_mismatch=dict(type='str', default='regenerate', choices=['regenerate', 'convert']), - select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'), - return_content=dict(type='bool', default=False), - regenerate=dict( - type='str', - default='full_idempotence', - choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always'] - ), - ), - supports_check_mode=True, - add_file_common_args=True, - required_together=[ - ['cipher', 'passphrase'] - ], - required_if=[ - ['type', 'ECC', ['curve']], - ], - ) - - base_dir = os.path.dirname(module.params['path']) or '.' - if not os.path.isdir(base_dir): - module.fail_json( - name=base_dir, - msg='The directory %s does not exist or the file is not a directory' % base_dir - ) - - 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_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) - - # Decision - if module.params['cipher'] and module.params['passphrase'] and module.params['cipher'] != 'auto': - # First try pyOpenSSL, then cryptography - if can_use_pyopenssl: - backend = 'pyopenssl' - elif can_use_cryptography: - backend = 'cryptography' - else: - # First try cryptography, then pyOpenSSL - if can_use_cryptography: - backend = 'cryptography' - elif can_use_pyopenssl: - backend = 'pyopenssl' - - # Success? - if backend == 'auto': - module.fail_json(msg=("Can't detect any of the required Python libraries " - "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( - MINIMAL_CRYPTOGRAPHY_VERSION, - MINIMAL_PYOPENSSL_VERSION)) - try: - if backend == 'pyopenssl': - if not PYOPENSSL_FOUND: - module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), - exception=PYOPENSSL_IMP_ERR) - module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') - private_key = PrivateKeyPyOpenSSL(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) - private_key = PrivateKeyCryptography(module) - - if private_key.state == 'present': - if module.check_mode: - result = private_key.dump() - result['changed'] = private_key.force \ - or not private_key.check(module, ignore_conversion=True) \ - or not private_key.check(module, ignore_conversion=False) - module.exit_json(**result) - - private_key.generate(module) - else: - if module.check_mode: - result = private_key.dump() - result['changed'] = os.path.exists(module.params['path']) - module.exit_json(**result) - - private_key.remove(module) - - result = private_key.dump() - module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: - module.fail_json(msg=to_native(exc)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/openssl_privatekey_info.py b/lib/ansible/modules/crypto/openssl_privatekey_info.py deleted file mode 100644 index c3f2b16dad..0000000000 --- a/lib/ansible/modules/crypto/openssl_privatekey_info.py +++ /dev/null @@ -1,651 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org> -# Copyright: (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: openssl_privatekey_info -version_added: '2.8' -short_description: Provide information for OpenSSL private keys -description: - - This module allows one to query information on OpenSSL private keys. - - In case the key consistency checks fail, the module will fail as this indicates a faked - private key. In this case, all return variables are still returned. Note that key consistency - checks are not available all key types; if none is available, C(none) is returned for - C(key_is_consistent). - - It uses the pyOpenSSL or cryptography python library to interact with OpenSSL. If both the - cryptography and PyOpenSSL libraries are available (and meet the minimum version requirements) - cryptography will be preferred as a backend over PyOpenSSL (unless the backend is forced with - C(select_crypto_backend)). Please note that the PyOpenSSL backend was deprecated in Ansible 2.9 - and will be removed in Ansible 2.13. -requirements: - - PyOpenSSL >= 0.15 or cryptography >= 1.2.3 -author: - - Felix Fontein (@felixfontein) - - Yanis Guenane (@Spredzy) -options: - path: - description: - - Remote absolute path where the private key file is loaded from. - type: path - content: - description: - - Content of the private key file. - - Either I(path) or I(content) must be specified, but not both. - type: str - version_added: "2.10" - 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(yes) when you want private information about this key to - leave the remote machine. - - "WARNING: you have to make sure that private key data isn't accidentally logged!" - type: bool - default: no - - 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(pyopenssl). - - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. - - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. - - Please note that the C(pyopenssl) backend has been deprecated in Ansible 2.9, and will be removed in Ansible 2.13. - From that point on, only the C(cryptography) backend will be available. - type: str - default: auto - choices: [ auto, cryptography, pyopenssl ] - -seealso: -- module: openssl_privatekey -''' - -EXAMPLES = r''' -- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) - openssl_privatekey: - path: /etc/ssl/private/ansible.com.pem - -- name: Get information on generated key - openssl_privatekey_info: - path: /etc/ssl/private/ansible.com.pem - register: result - -- name: Dump information - debug: - var: result -''' - -RETURN = r''' -can_load_key: - description: Whether the module was able to load the private key from disk - returned: always - type: bool -can_parse_key: - description: Whether the module was able to parse the private key - returned: always - type: bool -key_is_consistent: - description: - - Whether the key is consistent. Can also return C(none) next to C(yes) and - C(no), to indicate that consistency couldn't be checked. - - In case the check returns C(no), the module will fail. - returned: always - type: bool -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 -private_data: - description: - - Private key data. Depends on key type. - returned: success and when I(return_private_key_data) is set to C(yes) - type: dict -''' - - -import abc -import os -import traceback -from distutils.version import LooseVersion - -from ansible.module_utils import crypto as crypto_utils -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native, to_bytes - -MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' -MINIMAL_PYOPENSSL_VERSION = '0.15' - -PYOPENSSL_IMP_ERR = None -try: - import OpenSSL - from OpenSSL import crypto - PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) -except ImportError: - PYOPENSSL_IMP_ERR = traceback.format_exc() - PYOPENSSL_FOUND = False -else: - PYOPENSSL_FOUND = True - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - from cryptography.hazmat.primitives import serialization - CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) - try: - import cryptography.hazmat.primitives.asymmetric.x25519 - CRYPTOGRAPHY_HAS_X25519 = True - except ImportError: - CRYPTOGRAPHY_HAS_X25519 = False - try: - import cryptography.hazmat.primitives.asymmetric.x448 - CRYPTOGRAPHY_HAS_X448 = True - except ImportError: - CRYPTOGRAPHY_HAS_X448 = False - try: - import cryptography.hazmat.primitives.asymmetric.ed25519 - CRYPTOGRAPHY_HAS_ED25519 = True - except ImportError: - CRYPTOGRAPHY_HAS_ED25519 = False - try: - import cryptography.hazmat.primitives.asymmetric.ed448 - CRYPTOGRAPHY_HAS_ED448 = True - except ImportError: - CRYPTOGRAPHY_HAS_ED448 = False -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - -SIGNATURE_TEST_DATA = b'1234' - - -def _get_cryptography_key_info(key): - key_public_data = dict() - key_private_data = dict() - if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): - key_type = 'RSA' - key_public_data['size'] = key.key_size - key_public_data['modulus'] = key.public_key().public_numbers().n - key_public_data['exponent'] = key.public_key().public_numbers().e - key_private_data['p'] = key.private_numbers().p - key_private_data['q'] = key.private_numbers().q - key_private_data['exponent'] = key.private_numbers().d - elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): - key_type = 'DSA' - key_public_data['size'] = key.key_size - key_public_data['p'] = key.parameters().parameter_numbers().p - key_public_data['q'] = key.parameters().parameter_numbers().q - key_public_data['g'] = key.parameters().parameter_numbers().g - key_public_data['y'] = key.public_key().public_numbers().y - key_private_data['x'] = key.private_numbers().x - elif CRYPTOGRAPHY_HAS_X25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey): - key_type = 'X25519' - elif CRYPTOGRAPHY_HAS_X448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey): - key_type = 'X448' - elif CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): - key_type = 'Ed25519' - elif CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): - key_type = 'Ed448' - elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): - key_type = 'ECC' - key_public_data['curve'] = key.public_key().curve.name - key_public_data['x'] = key.public_key().public_numbers().x - key_public_data['y'] = key.public_key().public_numbers().y - key_public_data['exponent_size'] = key.public_key().curve.key_size - key_private_data['multiplier'] = key.private_numbers().private_value - else: - key_type = 'unknown ({0})'.format(type(key)) - return key_type, key_public_data, key_private_data - - -def _check_dsa_consistency(key_public_data, key_private_data): - # Get parameters - p = key_public_data.get('p') - q = key_public_data.get('q') - g = key_public_data.get('g') - y = key_public_data.get('y') - x = key_private_data.get('x') - for v in (p, q, g, y, x): - if v is None: - return None - # Make sure that g is not 0, 1 or -1 in Z/pZ - if g < 2 or g >= p - 1: - return False - # Make sure that x is in range - if x < 1 or x >= q: - return False - # Check whether q divides p-1 - if (p - 1) % q != 0: - return False - # Check that g**q mod p == 1 - if crypto_utils.binary_exp_mod(g, q, p) != 1: - return False - # Check whether g**x mod p == y - if crypto_utils.binary_exp_mod(g, x, p) != y: - return False - # Check (quickly) whether p or q are not primes - if crypto_utils.quick_is_not_prime(q) or crypto_utils.quick_is_not_prime(p): - return False - return True - - -def _is_cryptography_key_consistent(key, key_public_data, key_private_data): - if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey): - return bool(key._backend._lib.RSA_check_key(key._rsa_cdata)) - if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey): - result = _check_dsa_consistency(key_public_data, key_private_data) - if result is not None: - return result - try: - signature = key.sign(SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256()) - except AttributeError: - # sign() was added in cryptography 1.5, but we support older versions - return None - try: - key.public_key().verify( - signature, - SIGNATURE_TEST_DATA, - cryptography.hazmat.primitives.hashes.SHA256() - ) - return True - except cryptography.exceptions.InvalidSignature: - return False - if isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey): - try: - signature = key.sign( - SIGNATURE_TEST_DATA, - cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256()) - ) - except AttributeError: - # sign() was added in cryptography 1.5, but we support older versions - return None - try: - key.public_key().verify( - signature, - SIGNATURE_TEST_DATA, - cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256()) - ) - return True - except cryptography.exceptions.InvalidSignature: - return False - has_simple_sign_function = False - if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey): - has_simple_sign_function = True - if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey): - has_simple_sign_function = True - if has_simple_sign_function: - signature = key.sign(SIGNATURE_TEST_DATA) - try: - key.public_key().verify(signature, SIGNATURE_TEST_DATA) - return True - except cryptography.exceptions.InvalidSignature: - return False - # For X25519 and X448, there's no test yet. - return None - - -class PrivateKeyInfo(crypto_utils.OpenSSLObject): - def __init__(self, module, backend): - super(PrivateKeyInfo, self).__init__( - module.params['path'] or '', - 'present', - False, - module.check_mode, - ) - self.backend = backend - self.module = module - self.content = module.params['content'] - - self.passphrase = module.params['passphrase'] - self.return_private_key_data = module.params['return_private_key_data'] - - def generate(self): - # Empty method because crypto_utils.OpenSSLObject wants this - pass - - def dump(self): - # Empty method because crypto_utils.OpenSSLObject wants this - pass - - @abc.abstractmethod - def _get_public_key(self, binary): - pass - - @abc.abstractmethod - def _get_key_info(self): - pass - - @abc.abstractmethod - def _is_key_consistent(self, key_public_data, key_private_data): - pass - - def get_info(self): - result = dict( - can_load_key=False, - can_parse_key=False, - key_is_consistent=None, - ) - if self.content is not None: - priv_key_detail = self.content.encode('utf-8') - result['can_load_key'] = True - else: - try: - with open(self.path, 'rb') as b_priv_key_fh: - priv_key_detail = b_priv_key_fh.read() - result['can_load_key'] = True - except (IOError, OSError) as exc: - self.module.fail_json(msg=to_native(exc), **result) - try: - self.key = crypto_utils.load_privatekey( - path=None, - content=priv_key_detail, - passphrase=to_bytes(self.passphrase) if self.passphrase is not None else self.passphrase, - backend=self.backend - ) - result['can_parse_key'] = True - except crypto_utils.OpenSSLObjectError as exc: - self.module.fail_json(msg=to_native(exc), **result) - - result['public_key'] = self._get_public_key(binary=False) - pk = self._get_public_key(binary=True) - result['public_key_fingerprints'] = crypto_utils.get_fingerprint_of_bytes(pk) if pk is not None else dict() - - key_type, key_public_data, key_private_data = self._get_key_info() - result['type'] = key_type - result['public_data'] = key_public_data - if self.return_private_key_data: - result['private_data'] = key_private_data - - result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data) - if result['key_is_consistent'] is False: - # Only fail when it is False, to avoid to fail on None (which means "we don't know") - result['key_is_consistent'] = False - self.module.fail_json( - msg="Private key is not consistent! (See " - "https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)", - **result - ) - return result - - -class PrivateKeyInfoCryptography(PrivateKeyInfo): - """Validate the supplied private key, using the cryptography backend""" - def __init__(self, module): - super(PrivateKeyInfoCryptography, self).__init__(module, 'cryptography') - - def _get_public_key(self, binary): - return self.key.public_key().public_bytes( - serialization.Encoding.DER if binary else serialization.Encoding.PEM, - serialization.PublicFormat.SubjectPublicKeyInfo - ) - - def _get_key_info(self): - return _get_cryptography_key_info(self.key) - - def _is_key_consistent(self, key_public_data, key_private_data): - return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data) - - -class PrivateKeyInfoPyOpenSSL(PrivateKeyInfo): - """validate the supplied private key.""" - - def __init__(self, module): - super(PrivateKeyInfoPyOpenSSL, self).__init__(module, 'pyopenssl') - - def _get_public_key(self, binary): - try: - return crypto.dump_publickey( - crypto.FILETYPE_ASN1 if binary else crypto.FILETYPE_PEM, - self.key - ) - except AttributeError: - try: - # pyOpenSSL < 16.0: - bio = crypto._new_mem_buf() - if binary: - rc = crypto._lib.i2d_PUBKEY_bio(bio, self.key._pkey) - else: - rc = crypto._lib.PEM_write_bio_PUBKEY(bio, self.key._pkey) - if rc != 1: - crypto._raise_current_error() - return crypto._bio_to_string(bio) - except AttributeError: - self.module.warn('Your pyOpenSSL version does not support dumping public keys. ' - 'Please upgrade to version 16.0 or newer, or use the cryptography backend.') - - def bigint_to_int(self, bn): - '''Convert OpenSSL BIGINT to Python integer''' - if bn == OpenSSL._util.ffi.NULL: - return None - hexstr = OpenSSL._util.lib.BN_bn2hex(bn) - try: - return int(OpenSSL._util.ffi.string(hexstr), 16) - finally: - OpenSSL._util.lib.OPENSSL_free(hexstr) - - def _get_key_info(self): - key_public_data = dict() - key_private_data = dict() - openssl_key_type = self.key.type() - try_fallback = True - if crypto.TYPE_RSA == openssl_key_type: - key_type = 'RSA' - key_public_data['size'] = self.key.bits() - - try: - # Use OpenSSL directly to extract key data - key = OpenSSL._util.lib.EVP_PKEY_get1_RSA(self.key._pkey) - key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.RSA_free) - # OpenSSL 1.1 and newer have functions to extract the parameters - # from the EVP PKEY data structures. Older versions didn't have - # these getters, and it was common use to simply access the values - # directly. Since there's no guarantee that these data structures - # will still be accessible in the future, we use the getters for - # 1.1 and later, and directly access the values for 1.0.x and - # earlier. - if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000: - # Get modulus and exponents - n = OpenSSL._util.ffi.new("BIGNUM **") - e = OpenSSL._util.ffi.new("BIGNUM **") - d = OpenSSL._util.ffi.new("BIGNUM **") - OpenSSL._util.lib.RSA_get0_key(key, n, e, d) - key_public_data['modulus'] = self.bigint_to_int(n[0]) - key_public_data['exponent'] = self.bigint_to_int(e[0]) - key_private_data['exponent'] = self.bigint_to_int(d[0]) - # Get factors - p = OpenSSL._util.ffi.new("BIGNUM **") - q = OpenSSL._util.ffi.new("BIGNUM **") - OpenSSL._util.lib.RSA_get0_factors(key, p, q) - key_private_data['p'] = self.bigint_to_int(p[0]) - key_private_data['q'] = self.bigint_to_int(q[0]) - else: - # Get modulus and exponents - key_public_data['modulus'] = self.bigint_to_int(key.n) - key_public_data['exponent'] = self.bigint_to_int(key.e) - key_private_data['exponent'] = self.bigint_to_int(key.d) - # Get factors - key_private_data['p'] = self.bigint_to_int(key.p) - key_private_data['q'] = self.bigint_to_int(key.q) - try_fallback = False - except AttributeError: - # Use fallback if available - pass - elif crypto.TYPE_DSA == openssl_key_type: - key_type = 'DSA' - key_public_data['size'] = self.key.bits() - - try: - # Use OpenSSL directly to extract key data - key = OpenSSL._util.lib.EVP_PKEY_get1_DSA(self.key._pkey) - key = OpenSSL._util.ffi.gc(key, OpenSSL._util.lib.DSA_free) - # OpenSSL 1.1 and newer have functions to extract the parameters - # from the EVP PKEY data structures. Older versions didn't have - # these getters, and it was common use to simply access the values - # directly. Since there's no guarantee that these data structures - # will still be accessible in the future, we use the getters for - # 1.1 and later, and directly access the values for 1.0.x and - # earlier. - if OpenSSL.SSL.OPENSSL_VERSION_NUMBER >= 0x10100000: - # Get public parameters (primes and group element) - p = OpenSSL._util.ffi.new("BIGNUM **") - q = OpenSSL._util.ffi.new("BIGNUM **") - g = OpenSSL._util.ffi.new("BIGNUM **") - OpenSSL._util.lib.DSA_get0_pqg(key, p, q, g) - key_public_data['p'] = self.bigint_to_int(p[0]) - key_public_data['q'] = self.bigint_to_int(q[0]) - key_public_data['g'] = self.bigint_to_int(g[0]) - # Get public and private key exponents - y = OpenSSL._util.ffi.new("BIGNUM **") - x = OpenSSL._util.ffi.new("BIGNUM **") - OpenSSL._util.lib.DSA_get0_key(key, y, x) - key_public_data['y'] = self.bigint_to_int(y[0]) - key_private_data['x'] = self.bigint_to_int(x[0]) - else: - # Get public parameters (primes and group element) - key_public_data['p'] = self.bigint_to_int(key.p) - key_public_data['q'] = self.bigint_to_int(key.q) - key_public_data['g'] = self.bigint_to_int(key.g) - # Get public and private key exponents - key_public_data['y'] = self.bigint_to_int(key.pub_key) - key_private_data['x'] = self.bigint_to_int(key.priv_key) - try_fallback = False - except AttributeError: - # Use fallback if available - pass - else: - # Return 'unknown' - key_type = 'unknown ({0})'.format(self.key.type()) - # If needed and if possible, fall back to cryptography - if try_fallback and PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND: - return _get_cryptography_key_info(self.key.to_cryptography_key()) - return key_type, key_public_data, key_private_data - - def _is_key_consistent(self, key_public_data, key_private_data): - openssl_key_type = self.key.type() - if crypto.TYPE_RSA == openssl_key_type: - try: - return self.key.check() - except crypto.Error: - # OpenSSL error means that key is not consistent - return False - if crypto.TYPE_DSA == openssl_key_type: - result = _check_dsa_consistency(key_public_data, key_private_data) - if result is not None: - return result - signature = crypto.sign(self.key, SIGNATURE_TEST_DATA, 'sha256') - # Verify wants a cert (where it can get the public key from) - cert = crypto.X509() - cert.set_pubkey(self.key) - try: - crypto.verify(cert, signature, SIGNATURE_TEST_DATA, 'sha256') - return True - except crypto.Error: - return False - # If needed and if possible, fall back to cryptography - if PYOPENSSL_VERSION >= LooseVersion('16.1.0') and CRYPTOGRAPHY_FOUND: - return _is_cryptography_key_consistent(self.key.to_cryptography_key(), key_public_data, key_private_data) - return None - - -def main(): - module = AnsibleModule( - argument_spec=dict( - path=dict(type='path'), - content=dict(type='str'), - passphrase=dict(type='str', no_log=True), - return_private_key_data=dict(type='bool', default=False), - select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), - ), - required_one_of=( - ['path', 'content'], - ), - mutually_exclusive=( - ['path', 'content'], - ), - supports_check_mode=True, - ) - - try: - if module.params['path'] is not None: - base_dir = os.path.dirname(module.params['path']) or '.' - if not os.path.isdir(base_dir): - module.fail_json( - name=base_dir, - msg='The directory %s does not exist or the file is not a directory' % base_dir - ) - - backend = module.params['select_crypto_backend'] - if backend == 'auto': - # Detect what backend we can use - can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION) - can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) - - # If cryptography is available we'll use it - if can_use_cryptography: - backend = 'cryptography' - elif can_use_pyopenssl: - backend = 'pyopenssl' - - # Fail if no backend has been found - if backend == 'auto': - module.fail_json(msg=("Can't detect any of the required Python libraries " - "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( - MINIMAL_CRYPTOGRAPHY_VERSION, - MINIMAL_PYOPENSSL_VERSION)) - - if backend == 'pyopenssl': - if not PYOPENSSL_FOUND: - module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), - exception=PYOPENSSL_IMP_ERR) - module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') - privatekey = PrivateKeyInfoPyOpenSSL(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) - privatekey = PrivateKeyInfoCryptography(module) - - result = privatekey.get_info() - module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: - module.fail_json(msg=to_native(exc)) - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/crypto/openssl_publickey.py b/lib/ansible/modules/crypto/openssl_publickey.py deleted file mode 100644 index 6526b6fe93..0000000000 --- a/lib/ansible/modules/crypto/openssl_publickey.py +++ /dev/null @@ -1,478 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2016, Yanis Guenane <yanis+ansible@guenane.org> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: openssl_publickey -version_added: "2.3" -short_description: Generate an OpenSSL public key from its private key. -description: - - This module allows one to (re)generate OpenSSL public keys from their private keys. - - Keys are generated in PEM or OpenSSH format. - - The module can use the cryptography Python library, or the pyOpenSSL Python - library. By default, it tries to detect which one is available. This can be - overridden with the I(select_crypto_backend) option. When I(format) is C(OpenSSH), - the C(cryptography) backend has to be used. Please note that the PyOpenSSL backend - was deprecated in Ansible 2.9 and will be removed in Ansible 2.13." -requirements: - - Either cryptography >= 1.2.3 (older versions might work as well) - - Or pyOpenSSL >= 16.0.0 - - Needs cryptography >= 1.4 if I(format) is C(OpenSSH) -author: - - Yanis Guenane (@Spredzy) - - Felix Fontein (@felixfontein) -options: - state: - description: - - Whether the public key should exist or not, taking action if the state is different from what is stated. - type: str - default: present - choices: [ absent, present ] - force: - description: - - Should the key be regenerated even it it already exists. - type: bool - default: no - format: - description: - - The format of the public key. - type: str - default: PEM - choices: [ OpenSSH, PEM ] - version_added: "2.4" - path: - description: - - Name of the file in which the generated TLS/SSL public key will be written. - type: path - required: true - privatekey_path: - description: - - Path to the TLS/SSL private key from which to generate the public key. - - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both. - If I(state) is C(present), one of them is required. - type: path - privatekey_content: - description: - - The content of the TLS/SSL private key from which to generate the public key. - - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both. - If I(state) is C(present), one of them is required. - type: str - version_added: "2.10" - privatekey_passphrase: - description: - - The passphrase for the private key. - type: str - version_added: "2.4" - backup: - description: - - Create a backup file including a timestamp so you can get the original - public key back if you overwrote it with a different one by accident. - 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(pyopenssl). - - If set to C(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library. - - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library. - type: str - default: auto - choices: [ auto, cryptography, pyopenssl ] - version_added: "2.9" - return_content: - description: - - If set to C(yes), will return the (current or generated) public key's content as I(publickey). - type: bool - default: no - version_added: "2.10" -extends_documentation_fragment: -- files -seealso: -- module: openssl_certificate -- module: openssl_csr -- module: openssl_dhparam -- module: openssl_pkcs12 -- module: openssl_privatekey -''' - -EXAMPLES = r''' -- name: Generate an OpenSSL public key in PEM format - openssl_publickey: - path: /etc/ssl/public/ansible.com.pem - privatekey_path: /etc/ssl/private/ansible.com.pem - -- name: Generate an OpenSSL public key in PEM format from an inline key - openssl_publickey: - path: /etc/ssl/public/ansible.com.pem - privatekey_content: "{{ private_key_content }}" - -- name: Generate an OpenSSL public key in OpenSSH v2 format - openssl_publickey: - path: /etc/ssl/public/ansible.com.pem - privatekey_path: /etc/ssl/private/ansible.com.pem - format: OpenSSH - -- name: Generate an OpenSSL public key with a passphrase protected private key - openssl_publickey: - path: /etc/ssl/public/ansible.com.pem - privatekey_path: /etc/ssl/private/ansible.com.pem - privatekey_passphrase: ansible - -- name: Force regenerate an OpenSSL public key if it already exists - openssl_publickey: - path: /etc/ssl/public/ansible.com.pem - privatekey_path: /etc/ssl/private/ansible.com.pem - force: yes - -- name: Remove an OpenSSL public key - openssl_publickey: - path: /etc/ssl/public/ansible.com.pem - state: absent -''' - -RETURN = r''' -privatekey: - description: - - Path to the TLS/SSL private key the public key was generated from. - - Will be C(none) if the private key has been provided in I(privatekey_content). - returned: changed or success - type: str - sample: /etc/ssl/private/ansible.com.pem -format: - description: The format of the public key (PEM, OpenSSH, ...). - returned: changed or success - type: str - sample: PEM -filename: - description: Path to the generated TLS/SSL public key file. - returned: changed or success - type: str - sample: /etc/ssl/public/ansible.com.pem -fingerprint: - description: - - The fingerprint of the public key. Fingerprint will be generated for each hashlib.algorithms available. - - Requires PyOpenSSL >= 16.0 for meaningful output. - returned: changed or success - type: dict - sample: - md5: "84:75:71:72:8d:04:b5:6c:4d:37:6d:66:83:f5:4c:29" - sha1: "51:cc:7c:68:5d:eb:41:43:88:7e:1a:ae:c7:f8:24:72:ee:71:f6:10" - sha224: "b1:19:a6:6c:14:ac:33:1d:ed:18:50:d3:06:5c:b2:32:91:f1:f1:52:8c:cb:d5:75:e9:f5:9b:46" - sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7" - sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d" - sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b" -backup_file: - description: Name of backup file created. - returned: changed and if I(backup) is C(yes) - type: str - sample: /path/to/publickey.pem.2019-03-09@11:22~ -publickey: - description: The (current or generated) public key's content. - returned: if I(state) is C(present) and I(return_content) is C(yes) - type: str - version_added: "2.10" -''' - -import os -import traceback -from distutils.version import LooseVersion - -MINIMAL_PYOPENSSL_VERSION = '16.0.0' -MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3' -MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = '1.4' - -PYOPENSSL_IMP_ERR = None -try: - import OpenSSL - from OpenSSL import crypto - PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__) -except ImportError: - PYOPENSSL_IMP_ERR = traceback.format_exc() - PYOPENSSL_FOUND = False -else: - PYOPENSSL_FOUND = True - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import serialization as crypto_serialization - CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - -from ansible.module_utils import crypto as crypto_utils -from ansible.module_utils._text import to_native -from ansible.module_utils.basic import AnsibleModule, missing_required_lib - - -class PublicKeyError(crypto_utils.OpenSSLObjectError): - pass - - -class PublicKey(crypto_utils.OpenSSLObject): - - def __init__(self, module, backend): - super(PublicKey, self).__init__( - module.params['path'], - module.params['state'], - module.params['force'], - module.check_mode - ) - self.format = module.params['format'] - self.privatekey_path = module.params['privatekey_path'] - self.privatekey_content = module.params['privatekey_content'] - if self.privatekey_content is not None: - self.privatekey_content = self.privatekey_content.encode('utf-8') - self.privatekey_passphrase = module.params['privatekey_passphrase'] - self.privatekey = None - self.publickey_bytes = None - self.return_content = module.params['return_content'] - self.fingerprint = {} - self.backend = backend - - self.backup = module.params['backup'] - self.backup_file = None - - def _create_publickey(self, module): - self.privatekey = crypto_utils.load_privatekey( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase, - backend=self.backend - ) - if self.backend == 'cryptography': - if self.format == 'OpenSSH': - return self.privatekey.public_key().public_bytes( - crypto_serialization.Encoding.OpenSSH, - crypto_serialization.PublicFormat.OpenSSH - ) - else: - return self.privatekey.public_key().public_bytes( - crypto_serialization.Encoding.PEM, - crypto_serialization.PublicFormat.SubjectPublicKeyInfo - ) - else: - try: - return crypto.dump_publickey(crypto.FILETYPE_PEM, self.privatekey) - except AttributeError as dummy: - raise PublicKeyError('You need to have PyOpenSSL>=16.0.0 to generate public keys') - - def generate(self, module): - """Generate the public key.""" - - if self.privatekey_content is None and not os.path.exists(self.privatekey_path): - raise PublicKeyError( - 'The private key %s does not exist' % self.privatekey_path - ) - - if not self.check(module, perms_required=False) or self.force: - try: - publickey_content = self._create_publickey(module) - if self.return_content: - self.publickey_bytes = publickey_content - - if self.backup: - self.backup_file = module.backup_local(self.path) - crypto_utils.write_file(module, publickey_content) - - self.changed = True - except crypto_utils.OpenSSLBadPassphraseError as exc: - raise PublicKeyError(exc) - except (IOError, OSError) as exc: - raise PublicKeyError(exc) - - self.fingerprint = crypto_utils.get_fingerprint( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase, - backend=self.backend, - ) - file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, False): - self.changed = True - - def check(self, module, perms_required=True): - """Ensure the resource is in its desired state.""" - - state_and_perms = super(PublicKey, self).check(module, perms_required) - - def _check_privatekey(): - if self.privatekey_content is None and not os.path.exists(self.privatekey_path): - return False - - try: - with open(self.path, 'rb') as public_key_fh: - publickey_content = public_key_fh.read() - if self.return_content: - self.publickey_bytes = publickey_content - if self.backend == 'cryptography': - if self.format == 'OpenSSH': - # Read and dump public key. Makes sure that the comment is stripped off. - current_publickey = crypto_serialization.load_ssh_public_key(publickey_content, backend=default_backend()) - publickey_content = current_publickey.public_bytes( - crypto_serialization.Encoding.OpenSSH, - crypto_serialization.PublicFormat.OpenSSH - ) - else: - current_publickey = crypto_serialization.load_pem_public_key(publickey_content, backend=default_backend()) - publickey_content = current_publickey.public_bytes( - crypto_serialization.Encoding.PEM, - crypto_serialization.PublicFormat.SubjectPublicKeyInfo - ) - else: - publickey_content = crypto.dump_publickey( - crypto.FILETYPE_PEM, - crypto.load_publickey(crypto.FILETYPE_PEM, publickey_content) - ) - except Exception as dummy: - return False - - try: - desired_publickey = self._create_publickey(module) - except crypto_utils.OpenSSLBadPassphraseError as exc: - raise PublicKeyError(exc) - - return publickey_content == desired_publickey - - if not state_and_perms: - return state_and_perms - - return _check_privatekey() - - def remove(self, module): - if self.backup: - self.backup_file = module.backup_local(self.path) - super(PublicKey, self).remove(module) - - def dump(self): - """Serialize the object into a dictionary.""" - - result = { - 'privatekey': self.privatekey_path, - 'filename': self.path, - 'format': self.format, - 'changed': self.changed, - 'fingerprint': self.fingerprint, - } - if self.backup_file: - result['backup_file'] = self.backup_file - if self.return_content: - if self.publickey_bytes is None: - self.publickey_bytes = crypto_utils.load_file_if_exists(self.path, ignore_errors=True) - result['publickey'] = self.publickey_bytes.decode('utf-8') if self.publickey_bytes else None - - return result - - -def main(): - - module = AnsibleModule( - argument_spec=dict( - state=dict(type='str', default='present', choices=['present', 'absent']), - force=dict(type='bool', default=False), - path=dict(type='path', required=True), - privatekey_path=dict(type='path'), - privatekey_content=dict(type='str'), - format=dict(type='str', default='PEM', choices=['OpenSSH', 'PEM']), - privatekey_passphrase=dict(type='str', no_log=True), - backup=dict(type='bool', default=False), - select_crypto_backend=dict(type='str', choices=['auto', 'pyopenssl', 'cryptography'], default='auto'), - return_content=dict(type='bool', default=False), - ), - supports_check_mode=True, - add_file_common_args=True, - required_if=[('state', 'present', ['privatekey_path', 'privatekey_content'], True)], - mutually_exclusive=( - ['privatekey_path', 'privatekey_content'], - ), - ) - - minimal_cryptography_version = MINIMAL_CRYPTOGRAPHY_VERSION - if module.params['format'] == 'OpenSSH': - minimal_cryptography_version = MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH - - 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_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION) - - # Decision - if can_use_cryptography: - backend = 'cryptography' - elif can_use_pyopenssl: - if module.params['format'] == 'OpenSSH': - module.fail_json( - msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH)), - exception=CRYPTOGRAPHY_IMP_ERR - ) - backend = 'pyopenssl' - - # Success? - if backend == 'auto': - module.fail_json(msg=("Can't detect any of the required Python libraries " - "cryptography (>= {0}) or PyOpenSSL (>= {1})").format( - minimal_cryptography_version, - MINIMAL_PYOPENSSL_VERSION)) - - if module.params['format'] == 'OpenSSH' and backend != 'cryptography': - module.fail_json(msg="Format OpenSSH requires the cryptography backend.") - - if backend == 'pyopenssl': - if not PYOPENSSL_FOUND: - module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), - exception=PYOPENSSL_IMP_ERR) - module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', version='2.13') - elif backend == 'cryptography': - if not CRYPTOGRAPHY_FOUND: - module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(minimal_cryptography_version)), - exception=CRYPTOGRAPHY_IMP_ERR) - - base_dir = os.path.dirname(module.params['path']) or '.' - if not os.path.isdir(base_dir): - module.fail_json( - name=base_dir, - msg="The directory '%s' does not exist or the file is not a directory" % base_dir - ) - - try: - public_key = PublicKey(module, backend) - - if public_key.state == 'present': - if module.check_mode: - result = public_key.dump() - result['changed'] = module.params['force'] or not public_key.check(module) - module.exit_json(**result) - - public_key.generate(module) - else: - if module.check_mode: - result = public_key.dump() - result['changed'] = os.path.exists(module.params['path']) - module.exit_json(**result) - - public_key.remove(module) - - result = public_key.dump() - module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: - module.fail_json(msg=to_native(exc)) - - -if __name__ == '__main__': - main() diff --git a/lib/ansible/modules/crypto/x509_crl.py b/lib/ansible/modules/crypto/x509_crl.py deleted file mode 100644 index ef601edadc..0000000000 --- a/lib/ansible/modules/crypto/x509_crl.py +++ /dev/null @@ -1,783 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2019, Felix Fontein <felix@fontein.de> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: x509_crl -version_added: "2.10" -short_description: Generate Certificate Revocation Lists (CRLs) -description: - - This module allows one to (re)generate or update Certificate Revocation Lists (CRLs). - - Certificates on the revocation list can be either specified via serial number and (optionally) their issuer, - or as a path to a certificate file in PEM format. -requirements: - - cryptography >= 1.2 -author: - - Felix Fontein (@felixfontein) -options: - state: - description: - - Whether the CRL file should exist or not, taking action if the state is different from what is stated. - type: str - default: present - choices: [ absent, present ] - - mode: - description: - - Defines how to process entries of existing CRLs. - - If set to C(generate), makes sure that the CRL has the exact set of revoked certificates - as specified in I(revoked_certificates). - - If set to C(update), makes sure that the CRL contains the revoked certificates from - I(revoked_certificates), but can also contain other revoked certificates. If the CRL file - already exists, all entries from the existing CRL will also be included in the new CRL. - When using C(update), you might be interested in setting I(ignore_timestamps) to C(yes). - type: str - default: generate - choices: [ generate, update ] - - force: - description: - - Should the CRL be forced to be regenerated. - type: bool - default: no - - backup: - description: - - Create a backup file including a timestamp so you can get the original - CRL back if you overwrote it with a new one by accident. - type: bool - default: no - - path: - description: - - Remote absolute path where the generated CRL file should be created or is already located. - type: path - required: yes - - privatekey_path: - description: - - Path to the CA's private key to use when signing the CRL. - - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both. - type: path - - privatekey_content: - description: - - The content of the CA's private key to use when signing the CRL. - - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both. - type: str - - privatekey_passphrase: - description: - - The passphrase for the I(privatekey_path). - - This is required if the private key is password protected. - type: str - - issuer: - description: - - Key/value pairs that will be present in the issuer name field of the CRL. - - If you need to specify more than one value with the same key, use a list as value. - - Required if I(state) is C(present). - type: dict - - last_update: - description: - - The point in time from which this CRL can be trusted. - - Time can be specified either as relative time or as absolute timestamp. - - Time will always be interpreted as UTC. - - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer - + C([w | d | h | m | s]) (e.g. C(+32w1d2h). - - Note that if using relative time this module is NOT idempotent, except when - I(ignore_timestamps) is set to C(yes). - type: str - default: "+0s" - - next_update: - description: - - "The absolute latest point in time by which this I(issuer) is expected to have issued - another CRL. Many clients will treat a CRL as expired once I(next_update) occurs." - - Time can be specified either as relative time or as absolute timestamp. - - Time will always be interpreted as UTC. - - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer - + C([w | d | h | m | s]) (e.g. C(+32w1d2h). - - Note that if using relative time this module is NOT idempotent, except when - I(ignore_timestamps) is set to C(yes). - - Required if I(state) is C(present). - type: str - - digest: - description: - - Digest algorithm to be used when signing the CRL. - type: str - default: sha256 - - revoked_certificates: - description: - - List of certificates to be revoked. - - Required if I(state) is C(present). - type: list - elements: dict - suboptions: - path: - description: - - Path to a certificate in PEM format. - - The serial number and issuer will be extracted from the certificate. - - Mutually exclusive with I(content) and I(serial_number). One of these three options - must be specified. - type: path - content: - description: - - Content of a certificate in PEM format. - - The serial number and issuer will be extracted from the certificate. - - Mutually exclusive with I(path) and I(serial_number). One of these three options - must be specified. - type: str - serial_number: - description: - - Serial number of the certificate. - - Mutually exclusive with I(path) and I(content). One of these three options must - be specified. - type: int - revocation_date: - description: - - The point in time the certificate was revoked. - - Time can be specified either as relative time or as absolute timestamp. - - Time will always be interpreted as UTC. - - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer - + C([w | d | h | m | s]) (e.g. C(+32w1d2h). - - Note that if using relative time this module is NOT idempotent, except when - I(ignore_timestamps) is set to C(yes). - type: str - default: "+0s" - issuer: - description: - - The certificate's issuer. - - "Example: C(DNS:ca.example.org)" - type: list - elements: str - issuer_critical: - description: - - Whether the certificate issuer extension should be critical. - type: bool - default: no - reason: - description: - - The value for the revocation reason extension. - type: str - choices: - - unspecified - - key_compromise - - ca_compromise - - affiliation_changed - - superseded - - cessation_of_operation - - certificate_hold - - privilege_withdrawn - - aa_compromise - - remove_from_crl - reason_critical: - description: - - Whether the revocation reason extension should be critical. - type: bool - default: no - invalidity_date: - description: - - The point in time it was known/suspected that the private key was compromised - or that the certificate otherwise became invalid. - - Time can be specified either as relative time or as absolute timestamp. - - Time will always be interpreted as UTC. - - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer - + C([w | d | h | m | s]) (e.g. C(+32w1d2h). - - Note that if using relative time this module is NOT idempotent. This will NOT - change when I(ignore_timestamps) is set to C(yes). - type: str - invalidity_date_critical: - description: - - Whether the invalidity date extension should be critical. - type: bool - default: no - - ignore_timestamps: - description: - - Whether the timestamps I(last_update), I(next_update) and I(revocation_date) (in - I(revoked_certificates)) should be ignored for idempotency checks. The timestamp - I(invalidity_date) in I(revoked_certificates) will never be ignored. - - Use this in combination with relative timestamps for these values to get idempotency. - type: bool - default: no - - return_content: - description: - - If set to C(yes), will return the (current or generated) CRL's content as I(crl). - type: bool - default: no - -extends_documentation_fragment: - - files - -notes: - - All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern. - - Date specified should be UTC. Minutes and seconds are mandatory. -''' - -EXAMPLES = r''' -- name: Generate a CRL - x509_crl: - path: /etc/ssl/my-ca.crl - privatekey_path: /etc/ssl/private/my-ca.pem - issuer: - CN: My CA - last_update: "+0s" - next_update: "+7d" - revoked_certificates: - - serial_number: 1234 - revocation_date: 20190331202428Z - issuer: - CN: My CA - - serial_number: 2345 - revocation_date: 20191013152910Z - reason: affiliation_changed - invalidity_date: 20191001000000Z - - path: /etc/ssl/crt/revoked-cert.pem - revocation_date: 20191010010203Z -''' - -RETURN = r''' -filename: - description: Path to the generated CRL - returned: changed or success - type: str - sample: /path/to/my-ca.crl -backup_file: - description: Name of backup file created. - returned: changed and if I(backup) is C(yes) - type: str - sample: /path/to/my-ca.crl.2019-03-09@11:22~ -privatekey: - description: Path to the private CA key - returned: changed or success - type: str - sample: /path/to/my-ca.pem -issuer: - description: - - The CRL's issuer. - - Note that for repeated values, only the last one will be returned. - returned: success - type: dict - sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}' -issuer_ordered: - description: The CRL's issuer as an ordered list of tuples. - returned: success - type: list - elements: list - sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]' -last_update: - description: The point in time from which this CRL can be trusted as ASN.1 TIME. - returned: success - type: str - sample: 20190413202428Z -next_update: - description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME. - returned: success - type: str - sample: 20190413202428Z -digest: - description: The signature algorithm used to sign the CRL. - returned: success - type: str - sample: sha256WithRSAEncryption -revoked_certificates: - description: List of certificates to be revoked. - returned: success - type: list - elements: dict - contains: - serial_number: - description: Serial number of the certificate. - type: int - sample: 1234 - revocation_date: - description: The point in time the certificate was revoked as ASN.1 TIME. - type: str - sample: 20190413202428Z - issuer: - description: The certificate's issuer. - type: list - elements: str - sample: '["DNS:ca.example.org"]' - issuer_critical: - description: Whether the certificate issuer extension is critical. - type: bool - sample: no - reason: - description: - - The value for the revocation reason extension. - - One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded), - C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and - C(remove_from_crl). - type: str - sample: key_compromise - reason_critical: - description: Whether the revocation reason extension is critical. - type: bool - sample: no - invalidity_date: - description: | - The point in time it was known/suspected that the private key was compromised - or that the certificate otherwise became invalid as ASN.1 TIME. - type: str - sample: 20190413202428Z - invalidity_date_critical: - description: Whether the invalidity date extension is critical. - type: bool - sample: no -crl: - description: The (current or generated) CRL's content. - returned: if I(state) is C(present) and I(return_content) is C(yes) - type: str -''' - - -import os -import traceback -from distutils.version import LooseVersion - -from ansible.module_utils import crypto as crypto_utils -from ansible.module_utils._text import to_native, to_text -from ansible.module_utils.basic import AnsibleModule, missing_required_lib - -MINIMAL_CRYPTOGRAPHY_VERSION = '1.2' - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - from cryptography import x509 - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives.serialization import Encoding - from cryptography.x509 import ( - CertificateRevocationListBuilder, - RevokedCertificateBuilder, - NameAttribute, - Name, - ) - CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - - -TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" - - -class CRLError(crypto_utils.OpenSSLObjectError): - pass - - -class CRL(crypto_utils.OpenSSLObject): - - def __init__(self, module): - super(CRL, self).__init__( - module.params['path'], - module.params['state'], - module.params['force'], - module.check_mode - ) - - self.update = module.params['mode'] == 'update' - self.ignore_timestamps = module.params['ignore_timestamps'] - self.return_content = module.params['return_content'] - self.crl_content = None - - self.privatekey_path = module.params['privatekey_path'] - self.privatekey_content = module.params['privatekey_content'] - if self.privatekey_content is not None: - self.privatekey_content = self.privatekey_content.encode('utf-8') - self.privatekey_passphrase = module.params['privatekey_passphrase'] - - self.issuer = crypto_utils.parse_name_field(module.params['issuer']) - self.issuer = [(entry[0], entry[1]) for entry in self.issuer if entry[1]] - - self.last_update = crypto_utils.get_relative_time_option(module.params['last_update'], 'last_update') - self.next_update = crypto_utils.get_relative_time_option(module.params['next_update'], 'next_update') - - self.digest = crypto_utils.select_message_digest(module.params['digest']) - if self.digest is None: - raise CRLError('The digest "{0}" is not supported'.format(module.params['digest'])) - - self.revoked_certificates = [] - for i, rc in enumerate(module.params['revoked_certificates']): - result = { - 'serial_number': None, - 'revocation_date': None, - 'issuer': None, - 'issuer_critical': False, - 'reason': None, - 'reason_critical': False, - 'invalidity_date': None, - 'invalidity_date_critical': False, - } - path_prefix = 'revoked_certificates[{0}].'.format(i) - if rc['path'] is not None or rc['content'] is not None: - # Load certificate from file or content - try: - if rc['content'] is not None: - rc['content'] = rc['content'].encode('utf-8') - cert = crypto_utils.load_certificate(rc['path'], content=rc['content'], backend='cryptography') - try: - result['serial_number'] = cert.serial_number - except AttributeError: - # The property was called "serial" before cryptography 1.4 - result['serial_number'] = cert.serial - except crypto_utils.OpenSSLObjectError as e: - if rc['content'] is not None: - module.fail_json( - msg='Cannot parse certificate from {0}content: {1}'.format(path_prefix, to_native(e)) - ) - else: - module.fail_json( - msg='Cannot read certificate "{1}" from {0}path: {2}'.format(path_prefix, rc['path'], to_native(e)) - ) - else: - # Specify serial_number (and potentially issuer) directly - result['serial_number'] = rc['serial_number'] - # All other options - if rc['issuer']: - result['issuer'] = [crypto_utils.cryptography_get_name(issuer) for issuer in rc['issuer']] - result['issuer_critical'] = rc['issuer_critical'] - result['revocation_date'] = crypto_utils.get_relative_time_option( - rc['revocation_date'], - path_prefix + 'revocation_date' - ) - if rc['reason']: - result['reason'] = crypto_utils.REVOCATION_REASON_MAP[rc['reason']] - result['reason_critical'] = rc['reason_critical'] - if rc['invalidity_date']: - result['invalidity_date'] = crypto_utils.get_relative_time_option( - rc['invalidity_date'], - path_prefix + 'invalidity_date' - ) - result['invalidity_date_critical'] = rc['invalidity_date_critical'] - self.revoked_certificates.append(result) - - self.module = module - - self.backup = module.params['backup'] - self.backup_file = None - - try: - self.privatekey = crypto_utils.load_privatekey( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase, - backend='cryptography' - ) - except crypto_utils.OpenSSLBadPassphraseError as exc: - raise CRLError(exc) - - self.crl = None - try: - with open(self.path, 'rb') as f: - data = f.read() - self.crl = x509.load_pem_x509_crl(data, default_backend()) - if self.return_content: - self.crl_content = data - except Exception as dummy: - self.crl_content = None - - def remove(self): - if self.backup: - self.backup_file = self.module.backup_local(self.path) - super(CRL, self).remove(self.module) - - def _compress_entry(self, entry): - if self.ignore_timestamps: - # Throw out revocation_date - return ( - entry['serial_number'], - tuple(entry['issuer']) if entry['issuer'] is not None else None, - entry['issuer_critical'], - entry['reason'], - entry['reason_critical'], - entry['invalidity_date'], - entry['invalidity_date_critical'], - ) - else: - return ( - entry['serial_number'], - entry['revocation_date'], - tuple(entry['issuer']) if entry['issuer'] is not None else None, - entry['issuer_critical'], - entry['reason'], - entry['reason_critical'], - entry['invalidity_date'], - entry['invalidity_date_critical'], - ) - - def check(self, perms_required=True): - """Ensure the resource is in its desired state.""" - - state_and_perms = super(CRL, self).check(self.module, perms_required) - - if not state_and_perms: - return False - - if self.crl is None: - return False - - if self.last_update != self.crl.last_update and not self.ignore_timestamps: - return False - if self.next_update != self.crl.next_update and not self.ignore_timestamps: - return False - if self.digest.name != self.crl.signature_hash_algorithm.name: - return False - - want_issuer = [(crypto_utils.cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer] - if want_issuer != [(sub.oid, sub.value) for sub in self.crl.issuer]: - return False - - old_entries = [self._compress_entry(crypto_utils.cryptography_decode_revoked_certificate(cert)) for cert in self.crl] - new_entries = [self._compress_entry(cert) for cert in self.revoked_certificates] - if self.update: - # We don't simply use a set so that duplicate entries are treated correctly - for entry in new_entries: - try: - old_entries.remove(entry) - except ValueError: - return False - else: - if old_entries != new_entries: - return False - - return True - - def _generate_crl(self): - backend = default_backend() - crl = CertificateRevocationListBuilder() - - try: - crl = crl.issuer_name(Name([ - NameAttribute(crypto_utils.cryptography_name_to_oid(entry[0]), to_text(entry[1])) - for entry in self.issuer - ])) - except ValueError as e: - raise CRLError(e) - - crl = crl.last_update(self.last_update) - crl = crl.next_update(self.next_update) - - if self.update and self.crl: - new_entries = set([self._compress_entry(entry) for entry in self.revoked_certificates]) - for entry in self.crl: - decoded_entry = self._compress_entry(crypto_utils.cryptography_decode_revoked_certificate(entry)) - if decoded_entry not in new_entries: - crl = crl.add_revoked_certificate(entry) - for entry in self.revoked_certificates: - revoked_cert = RevokedCertificateBuilder() - revoked_cert = revoked_cert.serial_number(entry['serial_number']) - revoked_cert = revoked_cert.revocation_date(entry['revocation_date']) - if entry['issuer'] is not None: - revoked_cert = revoked_cert.add_extension( - x509.CertificateIssuer([ - crypto_utils.cryptography_get_name(name) for name in self.entry['issuer'] - ]), - entry['issuer_critical'] - ) - if entry['reason'] is not None: - revoked_cert = revoked_cert.add_extension( - x509.CRLReason(entry['reason']), - entry['reason_critical'] - ) - if entry['invalidity_date'] is not None: - revoked_cert = revoked_cert.add_extension( - x509.InvalidityDate(entry['invalidity_date']), - entry['invalidity_date_critical'] - ) - crl = crl.add_revoked_certificate(revoked_cert.build(backend)) - - self.crl = crl.sign(self.privatekey, self.digest, backend=backend) - return self.crl.public_bytes(Encoding.PEM) - - def generate(self): - if not self.check(perms_required=False) or self.force: - result = self._generate_crl() - if self.return_content: - self.crl_content = result - if self.backup: - self.backup_file = self.module.backup_local(self.path) - crypto_utils.write_file(self.module, result) - self.changed = True - - file_args = self.module.load_file_common_arguments(self.module.params) - if self.module.set_fs_attributes_if_different(file_args, False): - self.changed = True - - def _dump_revoked(self, entry): - return { - 'serial_number': entry['serial_number'], - 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT), - 'issuer': - [crypto_utils.cryptography_decode_name(issuer) for issuer in entry['issuer']] - if entry['issuer'] is not None else None, - 'issuer_critical': entry['issuer_critical'], - 'reason': crypto_utils.REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None, - 'reason_critical': entry['reason_critical'], - 'invalidity_date': - entry['invalidity_date'].strftime(TIMESTAMP_FORMAT) - if entry['invalidity_date'] is not None else None, - 'invalidity_date_critical': entry['invalidity_date_critical'], - } - - def dump(self, check_mode=False): - result = { - 'changed': self.changed, - 'filename': self.path, - 'privatekey': self.privatekey_path, - 'last_update': None, - 'next_update': None, - 'digest': None, - 'issuer_ordered': None, - 'issuer': None, - 'revoked_certificates': [], - } - if self.backup_file: - result['backup_file'] = self.backup_file - - if check_mode: - result['last_update'] = self.last_update.strftime(TIMESTAMP_FORMAT) - result['next_update'] = self.next_update.strftime(TIMESTAMP_FORMAT) - # result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid) - result['digest'] = self.module.params['digest'] - result['issuer_ordered'] = self.issuer - result['issuer'] = {} - for k, v in self.issuer: - result['issuer'][k] = v - result['revoked_certificates'] = [] - for entry in self.revoked_certificates: - result['revoked_certificates'].append(self._dump_revoked(entry)) - elif self.crl: - result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT) - result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT) - try: - result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid) - except AttributeError: - # Older cryptography versions don't have signature_algorithm_oid yet - dotted = crypto_utils._obj2txt( - self.crl._backend._lib, - self.crl._backend._ffi, - self.crl._x509_crl.sig_alg.algorithm - ) - oid = x509.oid.ObjectIdentifier(dotted) - result['digest'] = crypto_utils.cryptography_oid_to_name(oid) - issuer = [] - for attribute in self.crl.issuer: - issuer.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) - result['issuer_ordered'] = issuer - result['issuer'] = {} - for k, v in issuer: - result['issuer'][k] = v - result['revoked_certificates'] = [] - for cert in self.crl: - entry = crypto_utils.cryptography_decode_revoked_certificate(cert) - result['revoked_certificates'].append(self._dump_revoked(entry)) - - if self.return_content: - result['crl'] = self.crl_content - - return result - - -def main(): - module = AnsibleModule( - argument_spec=dict( - state=dict(type='str', default='present', choices=['present', 'absent']), - mode=dict(type='str', default='generate', choices=['generate', 'update']), - force=dict(type='bool', default=False), - backup=dict(type='bool', default=False), - path=dict(type='path', required=True), - privatekey_path=dict(type='path'), - privatekey_content=dict(type='str'), - privatekey_passphrase=dict(type='str', no_log=True), - issuer=dict(type='dict'), - last_update=dict(type='str', default='+0s'), - next_update=dict(type='str'), - digest=dict(type='str', default='sha256'), - ignore_timestamps=dict(type='bool', default=False), - return_content=dict(type='bool', default=False), - revoked_certificates=dict( - type='list', - elements='dict', - options=dict( - path=dict(type='path'), - content=dict(type='str'), - serial_number=dict(type='int'), - revocation_date=dict(type='str', default='+0s'), - issuer=dict(type='list', elements='str'), - issuer_critical=dict(type='bool', default=False), - reason=dict( - type='str', - choices=[ - 'unspecified', 'key_compromise', 'ca_compromise', 'affiliation_changed', - 'superseded', 'cessation_of_operation', 'certificate_hold', - 'privilege_withdrawn', 'aa_compromise', 'remove_from_crl' - ] - ), - reason_critical=dict(type='bool', default=False), - invalidity_date=dict(type='str'), - invalidity_date_critical=dict(type='bool', default=False), - ), - required_one_of=[['path', 'content', 'serial_number']], - mutually_exclusive=[['path', 'content', 'serial_number']], - ), - ), - required_if=[ - ('state', 'present', ['privatekey_path', 'privatekey_content'], True), - ('state', 'present', ['issuer', 'next_update', 'revoked_certificates'], False), - ], - mutually_exclusive=( - ['privatekey_path', 'privatekey_content'], - ), - supports_check_mode=True, - add_file_common_args=True, - ) - - if not CRYPTOGRAPHY_FOUND: - module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), - exception=CRYPTOGRAPHY_IMP_ERR) - - try: - crl = CRL(module) - - if module.params['state'] == 'present': - if module.check_mode: - result = crl.dump(check_mode=True) - result['changed'] = module.params['force'] or not crl.check() - module.exit_json(**result) - - crl.generate() - else: - if module.check_mode: - result = crl.dump(check_mode=True) - result['changed'] = os.path.exists(module.params['path']) - module.exit_json(**result) - - crl.remove() - - result = crl.dump() - module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as exc: - module.fail_json(msg=to_native(exc)) - - -if __name__ == "__main__": - main() diff --git a/lib/ansible/modules/crypto/x509_crl_info.py b/lib/ansible/modules/crypto/x509_crl_info.py deleted file mode 100644 index b61db26ff1..0000000000 --- a/lib/ansible/modules/crypto/x509_crl_info.py +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2020, Felix Fontein <felix@fontein.de> -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -from __future__ import absolute_import, division, print_function -__metaclass__ = type - -ANSIBLE_METADATA = {'metadata_version': '1.1', - 'status': ['preview'], - 'supported_by': 'community'} - -DOCUMENTATION = r''' ---- -module: x509_crl_info -version_added: "2.10" -short_description: Retrieve information on Certificate Revocation Lists (CRLs) -description: - - This module allows one to retrieve information on Certificate Revocation Lists (CRLs). -requirements: - - cryptography >= 1.2 -author: - - Felix Fontein (@felixfontein) -options: - path: - description: - - Remote absolute path where the generated CRL file should be created or is already located. - - Either I(path) or I(content) must be specified, but not both. - type: path - content: - description: - - Content of the X.509 certificate in PEM format. - - Either I(path) or I(content) must be specified, but not both. - type: str - -notes: - - All timestamp values are provided in ASN.1 TIME format, i.e. following the C(YYYYMMDDHHMMSSZ) pattern. - They are all in UTC. -seealso: - - module: x509_crl -''' - -EXAMPLES = r''' -- name: Get information on CRL - x509_crl_info: - path: /etc/ssl/my-ca.crl - register: result - -- debug: - msg: "{{ result }}" -''' - -RETURN = r''' -issuer: - description: - - The CRL's issuer. - - Note that for repeated values, only the last one will be returned. - returned: success - type: dict - sample: '{"organizationName": "Ansible", "commonName": "ca.example.com"}' -issuer_ordered: - description: The CRL's issuer as an ordered list of tuples. - returned: success - type: list - elements: list - sample: '[["organizationName", "Ansible"], ["commonName": "ca.example.com"]]' -last_update: - description: The point in time from which this CRL can be trusted as ASN.1 TIME. - returned: success - type: str - sample: 20190413202428Z -next_update: - description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME. - returned: success - type: str - sample: 20190413202428Z -digest: - description: The signature algorithm used to sign the CRL. - returned: success - type: str - sample: sha256WithRSAEncryption -revoked_certificates: - description: List of certificates to be revoked. - returned: success - type: list - elements: dict - contains: - serial_number: - description: Serial number of the certificate. - type: int - sample: 1234 - revocation_date: - description: The point in time the certificate was revoked as ASN.1 TIME. - type: str - sample: 20190413202428Z - issuer: - description: The certificate's issuer. - type: list - elements: str - sample: '["DNS:ca.example.org"]' - issuer_critical: - description: Whether the certificate issuer extension is critical. - type: bool - sample: no - reason: - description: - - The value for the revocation reason extension. - - One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded), - C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and - C(remove_from_crl). - type: str - sample: key_compromise - reason_critical: - description: Whether the revocation reason extension is critical. - type: bool - sample: no - invalidity_date: - description: | - The point in time it was known/suspected that the private key was compromised - or that the certificate otherwise became invalid as ASN.1 TIME. - type: str - sample: 20190413202428Z - invalidity_date_critical: - description: Whether the invalidity date extension is critical. - type: bool - sample: no -''' - - -import traceback -from distutils.version import LooseVersion - -from ansible.module_utils import crypto as crypto_utils -from ansible.module_utils._text import to_native -from ansible.module_utils.basic import AnsibleModule, missing_required_lib - -MINIMAL_CRYPTOGRAPHY_VERSION = '1.2' - -CRYPTOGRAPHY_IMP_ERR = None -try: - import cryptography - from cryptography import x509 - from cryptography.hazmat.backends import default_backend - CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) -except ImportError: - CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() - CRYPTOGRAPHY_FOUND = False -else: - CRYPTOGRAPHY_FOUND = True - - -TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ" - - -class CRLError(crypto_utils.OpenSSLObjectError): - pass - - -class CRLInfo(crypto_utils.OpenSSLObject): - """The main module implementation.""" - - def __init__(self, module): - super(CRLInfo, self).__init__( - module.params['path'] or '', - 'present', - False, - module.check_mode - ) - - self.content = module.params['content'] - - self.module = module - - self.crl = None - if self.content is None: - try: - with open(self.path, 'rb') as f: - data = f.read() - except Exception as e: - self.module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e)) - else: - data = self.content.encode('utf-8') - - try: - self.crl = x509.load_pem_x509_crl(data, default_backend()) - except Exception as e: - self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e)) - - def _dump_revoked(self, entry): - return { - 'serial_number': entry['serial_number'], - 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT), - 'issuer': - [crypto_utils.cryptography_decode_name(issuer) for issuer in entry['issuer']] - if entry['issuer'] is not None else None, - 'issuer_critical': entry['issuer_critical'], - 'reason': crypto_utils.REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None, - 'reason_critical': entry['reason_critical'], - 'invalidity_date': - entry['invalidity_date'].strftime(TIMESTAMP_FORMAT) - if entry['invalidity_date'] is not None else None, - 'invalidity_date_critical': entry['invalidity_date_critical'], - } - - def get_info(self): - result = { - 'changed': False, - 'last_update': None, - 'next_update': None, - 'digest': None, - 'issuer_ordered': None, - 'issuer': None, - 'revoked_certificates': [], - } - - result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT) - result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT) - try: - result['digest'] = crypto_utils.cryptography_oid_to_name(self.crl.signature_algorithm_oid) - except AttributeError: - # Older cryptography versions don't have signature_algorithm_oid yet - dotted = crypto_utils._obj2txt( - self.crl._backend._lib, - self.crl._backend._ffi, - self.crl._x509_crl.sig_alg.algorithm - ) - oid = x509.oid.ObjectIdentifier(dotted) - result['digest'] = crypto_utils.cryptography_oid_to_name(oid) - issuer = [] - for attribute in self.crl.issuer: - issuer.append([crypto_utils.cryptography_oid_to_name(attribute.oid), attribute.value]) - result['issuer_ordered'] = issuer - result['issuer'] = {} - for k, v in issuer: - result['issuer'][k] = v - result['revoked_certificates'] = [] - for cert in self.crl: - entry = crypto_utils.cryptography_decode_revoked_certificate(cert) - result['revoked_certificates'].append(self._dump_revoked(entry)) - - return result - - def generate(self): - # Empty method because crypto_utils.OpenSSLObject wants this - pass - - def dump(self): - # Empty method because crypto_utils.OpenSSLObject wants this - pass - - -def main(): - module = AnsibleModule( - argument_spec=dict( - path=dict(type='path'), - content=dict(type='str'), - ), - required_one_of=( - ['path', 'content'], - ), - mutually_exclusive=( - ['path', 'content'], - ), - supports_check_mode=True, - ) - - if not CRYPTOGRAPHY_FOUND: - module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)), - exception=CRYPTOGRAPHY_IMP_ERR) - - try: - crl = CRLInfo(module) - result = crl.get_info() - module.exit_json(**result) - except crypto_utils.OpenSSLObjectError as e: - module.fail_json(msg=to_native(e)) - - -if __name__ == "__main__": - main() |