summaryrefslogtreecommitdiff
path: root/lib/ansible/modules
diff options
context:
space:
mode:
authorAnsible Core Team <info@ansible.com>2020-03-09 09:40:30 +0000
committerAnsible Core Team <info@ansible.com>2020-03-09 09:40:30 +0000
commitef24d794eedb4b947bcbaa2681c7fc9cdfe8ff23 (patch)
treef02583d93085b8bfc9343dd60937b395ebb92254 /lib/ansible/modules
parent9d2d1370382f0790b0d9049640e19781497e1456 (diff)
downloadansible-ef24d794eedb4b947bcbaa2681c7fc9cdfe8ff23.tar.gz
Migrated to community.crypto
Diffstat (limited to 'lib/ansible/modules')
l---------lib/ansible/modules/crypto/acme/_acme_account_facts.py1
-rw-r--r--lib/ansible/modules/crypto/acme/acme_account.py278
-rw-r--r--lib/ansible/modules/crypto/acme/acme_account_info.py301
-rw-r--r--lib/ansible/modules/crypto/acme/acme_certificate.py1265
-rw-r--r--lib/ansible/modules/crypto/acme/acme_certificate_revoke.py223
-rw-r--r--lib/ansible/modules/crypto/acme/acme_challenge_cert_helper.py299
-rw-r--r--lib/ansible/modules/crypto/acme/acme_inspect.py320
-rw-r--r--lib/ansible/modules/crypto/certificate_complete_chain.py350
-rw-r--r--lib/ansible/modules/crypto/entrust/ecs_certificate.py952
-rw-r--r--lib/ansible/modules/crypto/entrust/ecs_domain.py409
-rw-r--r--lib/ansible/modules/crypto/get_certificate.py371
-rw-r--r--lib/ansible/modules/crypto/luks_device.py794
-rw-r--r--lib/ansible/modules/crypto/openssh_cert.py590
-rw-r--r--lib/ansible/modules/crypto/openssh_keypair.py493
-rw-r--r--lib/ansible/modules/crypto/openssl_certificate.py2756
-rw-r--r--lib/ansible/modules/crypto/openssl_certificate_info.py863
-rw-r--r--lib/ansible/modules/crypto/openssl_csr.py1159
-rw-r--r--lib/ansible/modules/crypto/openssl_csr_info.py667
-rw-r--r--lib/ansible/modules/crypto/openssl_dhparam.py418
-rw-r--r--lib/ansible/modules/crypto/openssl_pkcs12.py470
-rw-r--r--lib/ansible/modules/crypto/openssl_privatekey.py943
-rw-r--r--lib/ansible/modules/crypto/openssl_privatekey_info.py651
-rw-r--r--lib/ansible/modules/crypto/openssl_publickey.py478
-rw-r--r--lib/ansible/modules/crypto/x509_crl.py783
-rw-r--r--lib/ansible/modules/crypto/x509_crl_info.py281
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()