From f7dbd61fa74fc4e3b61a05aaa283eba675533e92 Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 6 Apr 2020 14:34:24 +0200 Subject: [PATCH] Rename openssl_certificate* to x509_certificate* (#7) * Rename openssl_certificate* to x509_certificate*. * Update README. * Add redirects. * Also print warnings when using Ansible 2.9. * Adjust ignore-2.9.txt. * Update documentation. --- README.md | 4 +- meta/routing.yml | 13 +- plugins/module_utils/crypto.py | 2 +- plugins/modules/acme_account_info.py | 2 +- plugins/modules/ecs_domain.py | 2 +- plugins/modules/openssl_certificate.py | 2726 +--------------- plugins/modules/openssl_certificate_info.py | 864 +----- plugins/modules/openssl_csr.py | 2 +- plugins/modules/openssl_dhparam.py | 2 +- plugins/modules/openssl_pkcs12.py | 2 +- plugins/modules/openssl_privatekey.py | 2 +- plugins/modules/openssl_publickey.py | 2 +- plugins/modules/x509_certificate.py | 2733 +++++++++++++++++ plugins/modules/x509_certificate_info.py | 871 ++++++ .../targets/acme_certificate/tasks/impl.yml | 16 +- .../targets/acme_certificate/tasks/main.yml | 4 +- .../targets/openssl_pkcs12/tasks/impl.yml | 2 +- .../aliases | 0 .../meta/main.yml | 0 .../tasks/assertonly.yml | 20 +- .../tasks/expired.yml | 6 +- .../tasks/impl.yml | 0 .../tasks/main.yml | 0 .../tasks/ownca.yml | 74 +- .../tasks/removal.yml | 6 +- .../tasks/selfsigned.yml | 54 +- .../tests/validate_ownca.yml | 0 .../tests/validate_selfsigned.yml | 0 .../aliases | 0 .../files/cert1.pem | 0 .../meta/main.yml | 0 .../tasks/impl.yml | 12 +- .../tasks/main.yml | 2 +- .../test_plugins/jinja_compatibility.py | 0 .../targets/x509_crl/tasks/main.yml | 6 +- tests/sanity/ignore-2.9.txt | 2 +- 36 files changed, 3730 insertions(+), 3701 deletions(-) mode change 100644 => 120000 plugins/modules/openssl_certificate.py mode change 100644 => 120000 plugins/modules/openssl_certificate_info.py create mode 100644 plugins/modules/x509_certificate.py create mode 100644 plugins/modules/x509_certificate_info.py rename tests/integration/targets/{openssl_certificate => x509_certificate}/aliases (100%) rename tests/integration/targets/{openssl_certificate => x509_certificate}/meta/main.yml (100%) rename tests/integration/targets/{openssl_certificate => x509_certificate}/tasks/assertonly.yml (95%) rename tests/integration/targets/{openssl_certificate => x509_certificate}/tasks/expired.yml (96%) rename tests/integration/targets/{openssl_certificate => x509_certificate}/tasks/impl.yml (100%) rename tests/integration/targets/{openssl_certificate => x509_certificate}/tasks/main.yml (100%) rename tests/integration/targets/{openssl_certificate => x509_certificate}/tasks/ownca.yml (96%) rename tests/integration/targets/{openssl_certificate => x509_certificate}/tasks/removal.yml (96%) rename tests/integration/targets/{openssl_certificate => x509_certificate}/tasks/selfsigned.yml (96%) rename tests/integration/targets/{openssl_certificate => x509_certificate}/tests/validate_ownca.yml (100%) rename tests/integration/targets/{openssl_certificate => x509_certificate}/tests/validate_selfsigned.yml (100%) rename tests/integration/targets/{openssl_certificate_info => x509_certificate_info}/aliases (100%) rename tests/integration/targets/{openssl_certificate_info => x509_certificate_info}/files/cert1.pem (100%) rename tests/integration/targets/{openssl_certificate_info => x509_certificate_info}/meta/main.yml (100%) rename tests/integration/targets/{openssl_certificate_info => x509_certificate_info}/tasks/impl.yml (95%) rename tests/integration/targets/{openssl_certificate_info => x509_certificate_info}/tasks/main.yml (99%) rename tests/integration/targets/{openssl_certificate_info => x509_certificate_info}/test_plugins/jinja_compatibility.py (100%) diff --git a/README.md b/README.md index 6934d925..cab58871 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,6 @@ Most modules require a recent enough version of [the Python cryptography library ## Included content - OpenSSL / PKI modules: - - openssl_certificate_info - - openssl_certificate - openssl_csr_info - openssl_csr - openssl_dhparam @@ -29,6 +27,8 @@ Most modules require a recent enough version of [the Python cryptography library - openssl_privatekey_info - openssl_privatekey - openssl_publickey + - x509_certificate_info + - x509_certificate - x509_crl_info - x509_crl - certificate_complete_chain diff --git a/meta/routing.yml b/meta/routing.yml index 9d04a8ec..48f603fe 100644 --- a/meta/routing.yml +++ b/meta/routing.yml @@ -3,4 +3,15 @@ plugin_routing: acme_account_facts: deprecation: removal_date: TBD - warning_text: see plugin documentation for details + removal_version: '2.12' + warning_text: The 'community.crypto.acme_account_facts' module has been renamed to 'community.crypto.acme_account_info'. + openssl_certificate: + deprecation: + removal_date: TBD + removal_version: '2.14' + warning_text: The 'community.crypto.openssl_certificate' module has been renamed to 'community.crypto.x509_certificate' + openssl_certificate_info: + deprecation: + removal_date: TBD + removal_version: '2.14' + warning_text: The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info' diff --git a/plugins/module_utils/crypto.py b/plugins/module_utils/crypto.py index 5ea8bd2c..be331774 100644 --- a/plugins/module_utils/crypto.py +++ b/plugins/module_utils/crypto.py @@ -67,7 +67,7 @@ try: # general name objects (DNSName, IPAddress, ...), while providing overloaded # equality and string representation operations. This makes it impossible to # use them in hash-based data structures such as set or dict. Since we are - # actually doing that in openssl_certificate, and potentially in other code, + # actually doing that in x509_certificate, and potentially in other code, # we need to monkey-patch __hash__ for these classes to make sure our code # works fine. if LooseVersion(cryptography.__version__) < LooseVersion('2.1'): diff --git a/plugins/modules/acme_account_info.py b/plugins/modules/acme_account_info.py index 840805fd..5ae94d5f 100644 --- a/plugins/modules/acme_account_info.py +++ b/plugins/modules/acme_account_info.py @@ -259,7 +259,7 @@ def main(): ), supports_check_mode=True, ) - if module._name == 'acme_account_facts': + if module._name in ('acme_account_facts', 'community.crypto.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) diff --git a/plugins/modules/ecs_domain.py b/plugins/modules/ecs_domain.py index 6559e021..4f6fba54 100644 --- a/plugins/modules/ecs_domain.py +++ b/plugins/modules/ecs_domain.py @@ -78,7 +78,7 @@ options: - Only allowed if C(verification_method=email) type: str seealso: - - module: openssl_certificate + - module: x509_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. diff --git a/plugins/modules/openssl_certificate.py b/plugins/modules/openssl_certificate.py deleted file mode 100644 index da94be63..00000000 --- a/plugins/modules/openssl_certificate.py +++ /dev/null @@ -1,2725 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2016-2017, Yanis Guenane -# Copyright: (c) 2017, Markus Teufelberger -# 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 -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 - - 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 - - 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 - - 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 - - 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 - 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 - - 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 - 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 - - 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 - - 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 - - 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 - - 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 - - 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 - - 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 - - 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 - - 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://: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 - - 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 - - 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 - - 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 - - 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 ] - - 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 - - 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' ] - - 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 - - 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 - - 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 - - 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 - - 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 - - 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 - - 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 - - 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 - - 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 - - return_content: - description: - - If set to C(yes), will return the (current or generated) certificate's content as I(certificate). - 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. - - 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 - community.crypto.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 - community.crypto.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 - community.crypto.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 - community.crypto.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 - community.crypto.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: - -- community.crypto.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 - -- community.crypto.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 - -- community.crypto.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 - -- community.crypto.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 - community.crypto.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 - community.crypto.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 - community.crypto.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) - community.crypto.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 - community.crypto.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 - community.crypto.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 - community.crypto.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 - community.crypto.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 - community.crypto.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 - community.crypto.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 - community.crypto.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" -''' - - -import abc -import datetime -import time -import os -import tempfile -import traceback - -from distutils.version import LooseVersion -from random import randint - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib -from ansible.module_utils._text import to_native, to_bytes, to_text - -from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils -from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress -from ansible_collections.community.crypto.plugins.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/plugins/modules/openssl_certificate.py b/plugins/modules/openssl_certificate.py new file mode 120000 index 00000000..24768dc3 --- /dev/null +++ b/plugins/modules/openssl_certificate.py @@ -0,0 +1 @@ +x509_certificate.py \ No newline at end of file diff --git a/plugins/modules/openssl_certificate_info.py b/plugins/modules/openssl_certificate_info.py deleted file mode 100644 index 297510ed..00000000 --- a/plugins/modules/openssl_certificate_info.py +++ /dev/null @@ -1,863 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -# Copyright: (c) 2016-2017, Yanis Guenane -# Copyright: (c) 2017, Markus Teufelberger -# 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 -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 - 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 - community.crypto.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 - community.crypto.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 - community.crypto.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.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_collections.community.crypto.plugins.module_utils import crypto as crypto_utils -from ansible_collections.community.crypto.plugins.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/plugins/modules/openssl_certificate_info.py b/plugins/modules/openssl_certificate_info.py new file mode 120000 index 00000000..86aa4240 --- /dev/null +++ b/plugins/modules/openssl_certificate_info.py @@ -0,0 +1 @@ +x509_certificate_info.py \ No newline at end of file diff --git a/plugins/modules/openssl_csr.py b/plugins/modules/openssl_csr.py index 31ecaaa7..b5e7ba8c 100644 --- a/plugins/modules/openssl_csr.py +++ b/plugins/modules/openssl_csr.py @@ -277,7 +277,7 @@ notes: 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: x509_certificate - module: openssl_dhparam - module: openssl_pkcs12 - module: openssl_privatekey diff --git a/plugins/modules/openssl_dhparam.py b/plugins/modules/openssl_dhparam.py index f03bdcf1..00ddf31f 100644 --- a/plugins/modules/openssl_dhparam.py +++ b/plugins/modules/openssl_dhparam.py @@ -75,7 +75,7 @@ options: extends_documentation_fragment: - files seealso: -- module: openssl_certificate +- module: x509_certificate - module: openssl_csr - module: openssl_pkcs12 - module: openssl_privatekey diff --git a/plugins/modules/openssl_pkcs12.py b/plugins/modules/openssl_pkcs12.py index b5034c77..6bd055e8 100644 --- a/plugins/modules/openssl_pkcs12.py +++ b/plugins/modules/openssl_pkcs12.py @@ -101,7 +101,7 @@ options: extends_documentation_fragment: - files seealso: -- module: openssl_certificate +- module: x509_certificate - module: openssl_csr - module: openssl_dhparam - module: openssl_privatekey diff --git a/plugins/modules/openssl_privatekey.py b/plugins/modules/openssl_privatekey.py index 9e2c3164..35a56964 100644 --- a/plugins/modules/openssl_privatekey.py +++ b/plugins/modules/openssl_privatekey.py @@ -190,7 +190,7 @@ options: extends_documentation_fragment: - files seealso: -- module: openssl_certificate +- module: x509_certificate - module: openssl_csr - module: openssl_dhparam - module: openssl_pkcs12 diff --git a/plugins/modules/openssl_publickey.py b/plugins/modules/openssl_publickey.py index d76f25d4..10e89742 100644 --- a/plugins/modules/openssl_publickey.py +++ b/plugins/modules/openssl_publickey.py @@ -92,7 +92,7 @@ options: extends_documentation_fragment: - files seealso: -- module: openssl_certificate +- module: x509_certificate - module: openssl_csr - module: openssl_dhparam - module: openssl_pkcs12 diff --git a/plugins/modules/x509_certificate.py b/plugins/modules/x509_certificate.py new file mode 100644 index 00000000..5848cd68 --- /dev/null +++ b/plugins/modules/x509_certificate.py @@ -0,0 +1,2733 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016-2017, Yanis Guenane +# Copyright: (c) 2017, Markus Teufelberger +# 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_certificate +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(x509_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. + - Note that this module was called C(openssl_certificate) when included directly in Ansible up to version 2.9. + When moved to the collection C(community.crypto), it was renamed to M(x509_certificate). From Ansible 2.10 on, it can + still be used by the old short name (or by C(ansible.builtin.openssl_certificate)), which redirects to + C(community.crypto.x509_certificate). When using FQCNs or when using the + L(collections,https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#using-collections-in-a-playbook) + keyword, the new name M(x509_certificate) should be used to avoid a deprecation warning. +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(x509_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 + + 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 + + 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 + + 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 + + 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 + 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 + + 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 + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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://: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 + + 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 + + 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 + + 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 + + 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 ] + + 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 + + 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' ] + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + 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 + + return_content: + description: + - If set to C(yes), will return the (current or generated) certificate's content as I(certificate). + 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. + - For security reason, when you use C(ownca) provider, you should NOT run M(x509_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 + community.crypto.x509_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 + community.crypto.x509_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 + community.crypto.x509_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 + community.crypto.x509_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 + community.crypto.x509_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 x509_certificate_info, +# openssl_csr_info, openssl_privatekey_info and assert modules: + +- community.crypto.x509_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 + +- community.crypto.x509_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 + +- community.crypto.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 + +- community.crypto.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 + community.crypto.x509_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 + community.crypto.x509_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 + community.crypto.x509_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) + community.crypto.x509_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 + community.crypto.x509_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 + community.crypto.x509_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 + community.crypto.x509_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 + community.crypto.x509_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 + community.crypto.x509_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 + community.crypto.x509_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 + community.crypto.x509_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" +''' + + +import abc +import datetime +import time +import os +import tempfile +import traceback + +from distutils.version import LooseVersion +from random import randint + +from ansible.module_utils.basic import AnsibleModule, missing_required_lib +from ansible.module_utils._text import to_native, to_bytes, to_text + +from ansible_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.module_utils.compat import ipaddress as compat_ipaddress +from ansible_collections.community.crypto.plugins.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'], + ], + ) + if module._name == 'community.crypto.openssl_certificate': + module.deprecate("The 'community.crypto.openssl_certificate' module has been renamed to 'community.crypto.x509_certificate'", version='2.14') + + 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 'x509_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/plugins/modules/x509_certificate_info.py b/plugins/modules/x509_certificate_info.py new file mode 100644 index 00000000..fd0cf39c --- /dev/null +++ b/plugins/modules/x509_certificate_info.py @@ -0,0 +1,871 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016-2017, Yanis Guenane +# Copyright: (c) 2017, Markus Teufelberger +# 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_certificate_info +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. + - Note that this module was called C(openssl_certificate_info) when included directly in Ansible up to version 2.9. + When moved to the collection C(community.crypto), it was renamed to M(x509_certificate_info). From Ansible 2.10 on, it can + still be used by the old short name (or by C(ansible.builtin.openssl_certificate_info)), which redirects to + C(community.crypto.x509_certificate_info). When using FQCNs or when using the + L(collections,https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#using-collections-in-a-playbook) + keyword, the new name M(x509_certificate_info) should be used to avoid a deprecation warning. +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 + 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: x509_certificate +''' + +EXAMPLES = r''' +- name: Generate a Self Signed OpenSSL certificate + community.crypto.x509_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 + community.crypto.x509_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 (x509_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 + community.crypto.x509_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.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_collections.community.crypto.plugins.module_utils import crypto as crypto_utils +from ansible_collections.community.crypto.plugins.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, + ) + if module._name == 'community.crypto.openssl_certificate_info': + module.deprecate("The 'community.crypto.openssl_certificate_info' module has been renamed to 'community.crypto.x509_certificate_info'", version='2.14') + + 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/tests/integration/targets/acme_certificate/tasks/impl.yml b/tests/integration/targets/acme_certificate/tasks/impl.yml index 04f02710..7592f57e 100644 --- a/tests/integration/targets/acme_certificate/tasks/impl.yml +++ b/tests/integration/targets/acme_certificate/tasks/impl.yml @@ -372,35 +372,35 @@ register: cert_8_text # Dump certificate info - name: Dumping cert 1 - openssl_certificate_info: + x509_certificate_info: path: "{{ output_dir }}/cert-1.pem" register: cert_1_info - name: Dumping cert 2 - openssl_certificate_info: + x509_certificate_info: path: "{{ output_dir }}/cert-2.pem" register: cert_2_info - name: Dumping cert 3 - openssl_certificate_info: + x509_certificate_info: path: "{{ output_dir }}/cert-3.pem" register: cert_3_info - name: Dumping cert 4 - openssl_certificate_info: + x509_certificate_info: path: "{{ output_dir }}/cert-4.pem" register: cert_4_info - name: Dumping cert 5 - openssl_certificate_info: + x509_certificate_info: path: "{{ output_dir }}/cert-5.pem" register: cert_5_info - name: Dumping cert 6 - openssl_certificate_info: + x509_certificate_info: path: "{{ output_dir }}/cert-6.pem" register: cert_6_info - name: Dumping cert 7 - openssl_certificate_info: + x509_certificate_info: path: "{{ output_dir }}/cert-7.pem" register: cert_7_info - name: Dumping cert 8 - openssl_certificate_info: + x509_certificate_info: path: "{{ output_dir }}/cert-8.pem" register: cert_8_info ## GET ACCOUNT ORDERS ######################################################################### diff --git a/tests/integration/targets/acme_certificate/tasks/main.yml b/tests/integration/targets/acme_certificate/tasks/main.yml index da66f5f3..8ef37ba8 100644 --- a/tests/integration/targets/acme_certificate/tasks/main.yml +++ b/tests/integration/targets/acme_certificate/tasks/main.yml @@ -7,13 +7,13 @@ loop: "{{ query('nested', types, root_numbers) }}" - name: Analyze root certificates - openssl_certificate_info: + x509_certificate_info: path: "{{ output_dir }}/acme-root-{{ item }}.pem" loop: "{{ root_numbers }}" register: acme_roots - name: Analyze intermediate certificates - openssl_certificate_info: + x509_certificate_info: path: "{{ output_dir }}/acme-intermediate-{{ item }}.pem" loop: "{{ root_numbers }}" register: acme_intermediates diff --git a/tests/integration/targets/openssl_pkcs12/tasks/impl.yml b/tests/integration/targets/openssl_pkcs12/tasks/impl.yml index c56b165e..f169f470 100644 --- a/tests/integration/targets/openssl_pkcs12/tasks/impl.yml +++ b/tests/integration/targets/openssl_pkcs12/tasks/impl.yml @@ -24,7 +24,7 @@ privatekey_path: '{{ output_dir }}/ansible_pkey3.pem' commonName: www3.ansible.com - name: Generate certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/{{ item.name }}.crt' privatekey_path: '{{ output_dir }}/{{ item.pkey }}' csr_path: '{{ output_dir }}/{{ item.name }}.csr' diff --git a/tests/integration/targets/openssl_certificate/aliases b/tests/integration/targets/x509_certificate/aliases similarity index 100% rename from tests/integration/targets/openssl_certificate/aliases rename to tests/integration/targets/x509_certificate/aliases diff --git a/tests/integration/targets/openssl_certificate/meta/main.yml b/tests/integration/targets/x509_certificate/meta/main.yml similarity index 100% rename from tests/integration/targets/openssl_certificate/meta/main.yml rename to tests/integration/targets/x509_certificate/meta/main.yml diff --git a/tests/integration/targets/openssl_certificate/tasks/assertonly.yml b/tests/integration/targets/x509_certificate/tasks/assertonly.yml similarity index 95% rename from tests/integration/targets/openssl_certificate/tasks/assertonly.yml rename to tests/integration/targets/x509_certificate/tasks/assertonly.yml index 994e00df..6cabccaa 100644 --- a/tests/integration/targets/openssl_certificate/tasks/assertonly.yml +++ b/tests/integration/targets/x509_certificate/tasks/assertonly.yml @@ -31,7 +31,7 @@ useCommonNameForSAN: no - name: (Assertonly, {{select_crypto_backend}}) - Generate selfsigned certificate (no extensions) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_noext.pem' csr_path: '{{ output_dir }}/csr_noext.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -40,7 +40,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' - name: (Assertonly, {{select_crypto_backend}}) - Generate selfsigned certificate (with SANs) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_sans.pem' csr_path: '{{ output_dir }}/csr_sans.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -49,7 +49,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' - name: (Assertonly, {{select_crypto_backend}}) - Assert that subject_alt_name is there (should fail) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_noext.pem' provider: assertonly subject_alt_name: @@ -59,7 +59,7 @@ register: extension_missing_san - name: (Assertonly, {{select_crypto_backend}}) - Assert that subject_alt_name is there - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_sans.pem' provider: assertonly subject_alt_name: @@ -70,7 +70,7 @@ register: extension_san - name: (Assertonly, {{select_crypto_backend}}) - Assert that subject_alt_name is there (strict) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_sans.pem' provider: assertonly subject_alt_name: @@ -82,7 +82,7 @@ register: extension_san_strict - name: (Assertonly, {{select_crypto_backend}}) - Assert that key_usage is there (should fail) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_noext.pem' provider: assertonly key_usage: @@ -92,7 +92,7 @@ register: extension_missing_ku - name: (Assertonly, {{select_crypto_backend}}) - Assert that extended_key_usage is there (should fail) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_noext.pem' provider: assertonly extended_key_usage: @@ -113,7 +113,7 @@ - "'Found no extendedKeyUsage extension' in extension_missing_eku.msg" - name: (Assertonly, {{select_crypto_backend}}) - Check private key passphrase fail 1 - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_noext.pem' privatekey_path: '{{ output_dir }}/privatekey.pem' privatekey_passphrase: hunter2 @@ -123,7 +123,7 @@ register: passphrase_error_1 - name: (Assertonly, {{select_crypto_backend}}) - Check private key passphrase fail 2 - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_noext.pem' privatekey_path: '{{ output_dir }}/privatekeypw.pem' privatekey_passphrase: wrong_password @@ -133,7 +133,7 @@ register: passphrase_error_2 - name: (Assertonly, {{select_crypto_backend}}) - Check private key passphrase fail 3 - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_noext.pem' privatekey_path: '{{ output_dir }}/privatekeypw.pem' provider: assertonly diff --git a/tests/integration/targets/openssl_certificate/tasks/expired.yml b/tests/integration/targets/x509_certificate/tasks/expired.yml similarity index 96% rename from tests/integration/targets/openssl_certificate/tasks/expired.yml rename to tests/integration/targets/x509_certificate/tasks/expired.yml index 248ce4a2..5a5d21b3 100644 --- a/tests/integration/targets/openssl_certificate/tasks/expired.yml +++ b/tests/integration/targets/x509_certificate/tasks/expired.yml @@ -11,7 +11,7 @@ commonName: www.example.com - name: (Expired, {{select_crypto_backend}}) Generate expired selfsigned certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/has_expired_cert.pem' csr_path: '{{ output_dir }}/has_expired_csr.csr' privatekey_path: '{{ output_dir }}/has_expired_privatekey.pem' @@ -27,7 +27,7 @@ when: select_crypto_backend == 'cryptography' # So we create it with 'command' - name: "(Expired) Check task fails because cert is expired (has_expired: false)" - openssl_certificate: + x509_certificate: provider: assertonly path: "{{ output_dir }}/has_expired_cert.pem" has_expired: false @@ -40,7 +40,7 @@ that: expired_cert_check is failed - name: "(Expired) Check expired cert check is ignored (has_expired: true)" - openssl_certificate: + x509_certificate: provider: assertonly path: "{{ output_dir }}/has_expired_cert.pem" has_expired: true diff --git a/tests/integration/targets/openssl_certificate/tasks/impl.yml b/tests/integration/targets/x509_certificate/tasks/impl.yml similarity index 100% rename from tests/integration/targets/openssl_certificate/tasks/impl.yml rename to tests/integration/targets/x509_certificate/tasks/impl.yml diff --git a/tests/integration/targets/openssl_certificate/tasks/main.yml b/tests/integration/targets/x509_certificate/tasks/main.yml similarity index 100% rename from tests/integration/targets/openssl_certificate/tasks/main.yml rename to tests/integration/targets/x509_certificate/tasks/main.yml diff --git a/tests/integration/targets/openssl_certificate/tasks/ownca.yml b/tests/integration/targets/x509_certificate/tasks/ownca.yml similarity index 96% rename from tests/integration/targets/openssl_certificate/tasks/ownca.yml rename to tests/integration/targets/x509_certificate/tasks/ownca.yml index 0d1381ff..ba2cc0ce 100644 --- a/tests/integration/targets/openssl_certificate/tasks/ownca.yml +++ b/tests/integration/targets/x509_certificate/tasks/ownca.yml @@ -34,7 +34,7 @@ basic_constraints_critical: yes - name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned CA certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ca_cert.pem' csr_path: '{{ output_dir }}/ca_csr.csr' privatekey_path: '{{ output_dir }}/ca_privatekey.pem' @@ -43,7 +43,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' - name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned CA certificate (privatekey passphrase) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ca_cert_pw.pem' csr_path: '{{ output_dir }}/ca_csr_pw.csr' privatekey_path: '{{ output_dir }}/ca_privatekey_pw.pem' @@ -53,7 +53,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert.pem' csr_path: '{{ output_dir }}/csr.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -66,7 +66,7 @@ register: ownca_certificate - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (idempotent) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert.pem' csr_path: '{{ output_dir }}/csr.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -79,7 +79,7 @@ register: ownca_certificate_idempotence - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (check mode) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert.pem' csr_path: '{{ output_dir }}/csr.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -91,7 +91,7 @@ check_mode: yes - name: (OwnCA, {{select_crypto_backend}}) Check ownca certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert.pem' privatekey_path: '{{ output_dir }}/privatekey.pem' provider: assertonly @@ -107,7 +107,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' - name: (OwnCA, {{select_crypto_backend}}) Generate ownca v2 certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_v2.pem' csr_path: '{{ output_dir }}/csr.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -121,7 +121,7 @@ ignore_errors: true - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate2 - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert2.pem' csr_path: '{{ output_dir }}/csr2.csr' privatekey_path: '{{ output_dir }}/privatekey2.pem' @@ -132,7 +132,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' - name: (OwnCA, {{select_crypto_backend}}) Check ownca certificate2 - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert2.pem' privatekey_path: '{{ output_dir }}/privatekey2.pem' provider: assertonly @@ -160,7 +160,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' - name: (OwnCA, {{select_crypto_backend}}) Create ownca certificate with notBefore and notAfter - openssl_certificate: + x509_certificate: provider: ownca ownca_not_before: 20181023133742Z ownca_not_after: 20191023133742Z @@ -172,7 +172,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' - name: (OwnCA, {{select_crypto_backend}}) Create ownca certificate with relative notBefore and notAfter - openssl_certificate: + x509_certificate: provider: ownca ownca_not_before: +1s ownca_not_after: +52w @@ -184,7 +184,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' - name: (OwnCA, {{select_crypto_backend}}) Generate ownca ECC certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_ecc.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' @@ -196,7 +196,7 @@ register: ownca_certificate_ecc - name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned certificate (privatekey passphrase) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_ecc_2.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert_pw.pem' @@ -208,7 +208,7 @@ register: selfsigned_certificate_passphrase - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (failed passphrase 1) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_pw1.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -221,7 +221,7 @@ register: passphrase_error_1 - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (failed passphrase 2) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_pw2.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -234,7 +234,7 @@ register: passphrase_error_2 - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (failed passphrase 3) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_pw3.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -250,7 +250,7 @@ dest: "{{ output_dir }}/ownca_broken.pem" content: "broken" - name: Regenerate broken cert - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_broken.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' @@ -261,7 +261,7 @@ register: ownca_broken - name: (OwnCA, {{select_crypto_backend}}) Backup test - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_backup.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -272,7 +272,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: ownca_backup_1 - name: (OwnCA, {{select_crypto_backend}}) Backup test (idempotent) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_backup.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -283,7 +283,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: ownca_backup_2 - name: (OwnCA, {{select_crypto_backend}}) Backup test (change) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_backup.pem' csr_path: '{{ output_dir }}/csr.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -294,7 +294,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: ownca_backup_3 - name: (OwnCA, {{select_crypto_backend}}) Backup test (remove) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_backup.pem' state: absent provider: ownca @@ -302,7 +302,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: ownca_backup_4 - name: (OwnCA, {{select_crypto_backend}}) Backup test (remove, idempotent) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_backup.pem' state: absent provider: ownca @@ -311,7 +311,7 @@ register: ownca_backup_5 - name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_ski.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -324,7 +324,7 @@ register: ownca_subject_key_identifier_1 - name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (idempotency) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_ski.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -337,7 +337,7 @@ register: ownca_subject_key_identifier_2 - name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (remove) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_ski.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -350,7 +350,7 @@ register: ownca_subject_key_identifier_3 - name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (remove idempotency) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_ski.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -363,7 +363,7 @@ register: ownca_subject_key_identifier_4 - name: (OwnCA, {{select_crypto_backend}}) Create subject key identifier (re-enable) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_ski.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -376,7 +376,7 @@ register: ownca_subject_key_identifier_5 - name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_aki.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -389,7 +389,7 @@ register: ownca_authority_key_identifier_1 - name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (idempotency) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_aki.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -402,7 +402,7 @@ register: ownca_authority_key_identifier_2 - name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (remove) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_aki.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -415,7 +415,7 @@ register: ownca_authority_key_identifier_3 - name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (remove idempotency) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_aki.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -428,7 +428,7 @@ register: ownca_authority_key_identifier_4 - name: (OwnCA, {{select_crypto_backend}}) Create authority key identifier (re-add) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_aki.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -469,7 +469,7 @@ ignore_errors: yes - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_{{ item }}.pem' csr_path: '{{ output_dir }}/csr_{{ item }}.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -484,7 +484,7 @@ ignore_errors: yes - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (idempotent) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_{{ item }}.pem' csr_path: '{{ output_dir }}/csr_{{ item }}.csr' ownca_path: '{{ output_dir }}/ca_cert.pem' @@ -529,7 +529,7 @@ ignore_errors: yes - name: (OwnCA, {{select_crypto_backend}}) Generate selfsigned CA certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ca_cert_{{ item }}.pem' csr_path: '{{ output_dir }}/ca_csr_{{ item }}.csr' privatekey_path: '{{ output_dir }}/ca_privatekey_{{ item }}.pem' @@ -542,7 +542,7 @@ ignore_errors: yes - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_{{ item }}_2.pem' csr_path: '{{ output_dir }}/csr.csr' ownca_path: '{{ output_dir }}/ca_cert_{{ item }}.pem' @@ -558,7 +558,7 @@ ignore_errors: yes - name: (OwnCA, {{select_crypto_backend}}) Generate ownca certificate (idempotent) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/ownca_cert_{{ item }}_2.pem' csr_path: '{{ output_dir }}/csr.csr' ownca_path: '{{ output_dir }}/ca_cert_{{ item }}.pem' diff --git a/tests/integration/targets/openssl_certificate/tasks/removal.yml b/tests/integration/targets/x509_certificate/tasks/removal.yml similarity index 96% rename from tests/integration/targets/openssl_certificate/tasks/removal.yml rename to tests/integration/targets/x509_certificate/tasks/removal.yml index e45e952d..8a310ecc 100644 --- a/tests/integration/targets/openssl_certificate/tasks/removal.yml +++ b/tests/integration/targets/x509_certificate/tasks/removal.yml @@ -9,7 +9,7 @@ privatekey_path: '{{ output_dir }}/removal_privatekey.pem' - name: (Removal, {{select_crypto_backend}}) Generate selfsigned certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/removal_cert.pem' csr_path: '{{ output_dir }}/removal_csr.csr' privatekey_path: '{{ output_dir }}/removal_privatekey.pem' @@ -23,7 +23,7 @@ register: removal_1_prestat - name: "(Removal, {{select_crypto_backend}}) Remove certificate" - openssl_certificate: + x509_certificate: path: "{{ output_dir }}/removal_cert.pem" state: absent select_crypto_backend: '{{ select_crypto_backend }}' @@ -36,7 +36,7 @@ register: removal_1_poststat - name: "(Removal, {{select_crypto_backend}}) Remove certificate (idempotent)" - openssl_certificate: + x509_certificate: path: "{{ output_dir }}/removal_cert.pem" state: absent select_crypto_backend: '{{ select_crypto_backend }}' diff --git a/tests/integration/targets/openssl_certificate/tasks/selfsigned.yml b/tests/integration/targets/x509_certificate/tasks/selfsigned.yml similarity index 96% rename from tests/integration/targets/openssl_certificate/tasks/selfsigned.yml rename to tests/integration/targets/x509_certificate/tasks/selfsigned.yml index 5455d40b..9c5039e8 100644 --- a/tests/integration/targets/openssl_certificate/tasks/selfsigned.yml +++ b/tests/integration/targets/x509_certificate/tasks/selfsigned.yml @@ -25,7 +25,7 @@ commonName: www.example.org - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert.pem' csr_path: '{{ output_dir }}/csr.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -36,7 +36,7 @@ register: selfsigned_certificate - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate - idempotency - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert.pem' csr_path: '{{ output_dir }}/csr.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -47,7 +47,7 @@ register: selfsigned_certificate_idempotence - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (check mode) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert.pem' csr_path: '{{ output_dir }}/csr.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -57,7 +57,7 @@ check_mode: yes - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (check mode, other CSR) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert.pem' csr_path: '{{ output_dir }}/csr_minimal_change.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -68,7 +68,7 @@ register: selfsigned_certificate_csr_minimal_change - name: (Selfsigned, {{select_crypto_backend}}) Check selfsigned certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert.pem' privatekey_path: '{{ output_dir }}/privatekey.pem' provider: assertonly @@ -82,7 +82,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned v2 certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_v2.pem' csr_path: '{{ output_dir }}/csr.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -117,7 +117,7 @@ - biometricInfo - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate2 - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert2.pem' csr_path: '{{ output_dir }}/csr2.csr' privatekey_path: '{{ output_dir }}/privatekey2.pem' @@ -126,7 +126,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' - name: (Selfsigned, {{select_crypto_backend}}) Check selfsigned certificate2 - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert2.pem' privatekey_path: '{{ output_dir }}/privatekey2.pem' provider: assertonly @@ -163,7 +163,7 @@ path: "{{ output_dir }}/csr3.pem" - name: (Selfsigned, {{select_crypto_backend}}) Create certificate3 with notBefore and notAfter - openssl_certificate: + x509_certificate: provider: selfsigned selfsigned_not_before: 20181023133742Z selfsigned_not_after: 20191023133742Z @@ -187,7 +187,7 @@ commonName: www.example.com - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_ecc.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' @@ -205,7 +205,7 @@ commonName: www.example.com - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (privatekey passphrase) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_pass.pem' csr_path: '{{ output_dir }}/csr_pass.csr' privatekey_path: '{{ output_dir }}/privatekeypw.pem' @@ -216,7 +216,7 @@ register: selfsigned_certificate_passphrase - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (failed passphrase 1) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_pw1.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -228,7 +228,7 @@ register: passphrase_error_1 - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (failed passphrase 2) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_pw2.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekeypw.pem' @@ -240,7 +240,7 @@ register: passphrase_error_2 - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate (failed passphrase 3) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_pw3.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekeypw.pem' @@ -255,7 +255,7 @@ dest: "{{ output_dir }}/cert_broken.pem" content: "broken" - name: Regenerate broken cert - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_broken.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' @@ -264,7 +264,7 @@ register: selfsigned_broken - name: (Selfsigned, {{select_crypto_backend}}) Backup test - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/selfsigned_cert_backup.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' @@ -274,7 +274,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: selfsigned_backup_1 - name: (Selfsigned, {{select_crypto_backend}}) Backup test (idempotent) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/selfsigned_cert_backup.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' @@ -284,7 +284,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: selfsigned_backup_2 - name: (Selfsigned, {{select_crypto_backend}}) Backup test (change) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/selfsigned_cert_backup.pem' csr_path: '{{ output_dir }}/csr.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' @@ -294,7 +294,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: selfsigned_backup_3 - name: (Selfsigned, {{select_crypto_backend}}) Backup test (remove) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/selfsigned_cert_backup.pem' state: absent provider: selfsigned @@ -302,7 +302,7 @@ select_crypto_backend: '{{ select_crypto_backend }}' register: selfsigned_backup_4 - name: (Selfsigned, {{select_crypto_backend}}) Backup test (remove, idempotent) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/selfsigned_cert_backup.pem' state: absent provider: selfsigned @@ -311,7 +311,7 @@ register: selfsigned_backup_5 - name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/selfsigned_cert_ski.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' @@ -323,7 +323,7 @@ register: selfsigned_subject_key_identifier_1 - name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (idempotency) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/selfsigned_cert_ski.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' @@ -335,7 +335,7 @@ register: selfsigned_subject_key_identifier_2 - name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (remove) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/selfsigned_cert_ski.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' @@ -347,7 +347,7 @@ register: selfsigned_subject_key_identifier_3 - name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (remove idempotency) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/selfsigned_cert_ski.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' @@ -359,7 +359,7 @@ register: selfsigned_subject_key_identifier_4 - name: (Selfsigned, {{select_crypto_backend}}) Create subject key identifier test (re-enable) - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/selfsigned_cert_ski.pem' csr_path: '{{ output_dir }}/csr_ecc.csr' privatekey_path: '{{ output_dir }}/privatekey_ecc.pem' @@ -399,7 +399,7 @@ ignore_errors: yes - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_{{ item }}.pem' csr_path: '{{ output_dir }}/csr_{{ item }}.csr' privatekey_path: '{{ output_dir }}/privatekey_{{ item }}.pem' @@ -413,7 +413,7 @@ ignore_errors: yes - name: (Selfsigned, {{select_crypto_backend}}) Generate selfsigned certificate - idempotency - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_{{ item }}.pem' csr_path: '{{ output_dir }}/csr_{{ item }}.csr' privatekey_path: '{{ output_dir }}/privatekey_{{ item }}.pem' diff --git a/tests/integration/targets/openssl_certificate/tests/validate_ownca.yml b/tests/integration/targets/x509_certificate/tests/validate_ownca.yml similarity index 100% rename from tests/integration/targets/openssl_certificate/tests/validate_ownca.yml rename to tests/integration/targets/x509_certificate/tests/validate_ownca.yml diff --git a/tests/integration/targets/openssl_certificate/tests/validate_selfsigned.yml b/tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml similarity index 100% rename from tests/integration/targets/openssl_certificate/tests/validate_selfsigned.yml rename to tests/integration/targets/x509_certificate/tests/validate_selfsigned.yml diff --git a/tests/integration/targets/openssl_certificate_info/aliases b/tests/integration/targets/x509_certificate_info/aliases similarity index 100% rename from tests/integration/targets/openssl_certificate_info/aliases rename to tests/integration/targets/x509_certificate_info/aliases diff --git a/tests/integration/targets/openssl_certificate_info/files/cert1.pem b/tests/integration/targets/x509_certificate_info/files/cert1.pem similarity index 100% rename from tests/integration/targets/openssl_certificate_info/files/cert1.pem rename to tests/integration/targets/x509_certificate_info/files/cert1.pem diff --git a/tests/integration/targets/openssl_certificate_info/meta/main.yml b/tests/integration/targets/x509_certificate_info/meta/main.yml similarity index 100% rename from tests/integration/targets/openssl_certificate_info/meta/main.yml rename to tests/integration/targets/x509_certificate_info/meta/main.yml diff --git a/tests/integration/targets/openssl_certificate_info/tasks/impl.yml b/tests/integration/targets/x509_certificate_info/tasks/impl.yml similarity index 95% rename from tests/integration/targets/openssl_certificate_info/tasks/impl.yml rename to tests/integration/targets/x509_certificate_info/tasks/impl.yml index 2ae89ed7..91804288 100644 --- a/tests/integration/targets/openssl_certificate_info/tasks/impl.yml +++ b/tests/integration/targets/x509_certificate_info/tasks/impl.yml @@ -3,7 +3,7 @@ msg: "Executing tests with backend {{ select_crypto_backend }}" - name: ({{select_crypto_backend}}) Get certificate info - openssl_certificate_info: + x509_certificate_info: path: '{{ output_dir }}/cert_1.pem' select_crypto_backend: '{{ select_crypto_backend }}' register: result @@ -36,7 +36,7 @@ info_results: "{{ info_results + [result] }}" - name: ({{select_crypto_backend}}) Get certificate info directly - openssl_certificate_info: + x509_certificate_info: content: '{{ lookup("file", output_dir ~ "/cert_1.pem") }}' select_crypto_backend: '{{ select_crypto_backend }}' register: result_direct @@ -47,7 +47,7 @@ - result == result_direct - name: ({{select_crypto_backend}}) Get certificate info - openssl_certificate_info: + x509_certificate_info: path: '{{ output_dir }}/cert_2.pem' select_crypto_backend: '{{ select_crypto_backend }}' valid_at: @@ -66,7 +66,7 @@ info_results: "{{ info_results + [result] }}" - name: ({{select_crypto_backend}}) Get certificate info - openssl_certificate_info: + x509_certificate_info: path: '{{ output_dir }}/cert_3.pem' select_crypto_backend: '{{ select_crypto_backend }}' register: result @@ -88,7 +88,7 @@ info_results: "{{ info_results + [result] }}" - name: ({{select_crypto_backend}}) Get certificate info - openssl_certificate_info: + x509_certificate_info: path: '{{ output_dir }}/cert_4.pem' select_crypto_backend: '{{ select_crypto_backend }}' register: result @@ -106,7 +106,7 @@ info_results: "{{ info_results + [result] }}" - name: ({{select_crypto_backend}}) Get certificate info for packaged cert 1 - openssl_certificate_info: + x509_certificate_info: path: '{{ role_path }}/files/cert1.pem' select_crypto_backend: '{{ select_crypto_backend }}' register: result diff --git a/tests/integration/targets/openssl_certificate_info/tasks/main.yml b/tests/integration/targets/x509_certificate_info/tasks/main.yml similarity index 99% rename from tests/integration/targets/openssl_certificate_info/tasks/main.yml rename to tests/integration/targets/x509_certificate_info/tasks/main.yml index 8fc2636c..b55b6b63 100644 --- a/tests/integration/targets/openssl_certificate_info/tasks/main.yml +++ b/tests/integration/targets/x509_certificate_info/tasks/main.yml @@ -113,7 +113,7 @@ authority_key_identifier: '{{ "44:55:66:77" if cryptography_version.stdout is version("1.3", ">=") else omit }}' - name: Generate selfsigned certificates - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/cert_{{ item }}.pem' csr_path: '{{ output_dir }}/csr_{{ item }}.csr' privatekey_path: '{{ output_dir }}/privatekey.pem' diff --git a/tests/integration/targets/openssl_certificate_info/test_plugins/jinja_compatibility.py b/tests/integration/targets/x509_certificate_info/test_plugins/jinja_compatibility.py similarity index 100% rename from tests/integration/targets/openssl_certificate_info/test_plugins/jinja_compatibility.py rename to tests/integration/targets/x509_certificate_info/test_plugins/jinja_compatibility.py diff --git a/tests/integration/targets/x509_crl/tasks/main.yml b/tests/integration/targets/x509_crl/tasks/main.yml index 1f82ff9e..779c1cfb 100644 --- a/tests/integration/targets/x509_crl/tasks/main.yml +++ b/tests/integration/targets/x509_crl/tasks/main.yml @@ -42,7 +42,7 @@ loop: "{{ certificates }}" - name: Generate CA certificates - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/{{ item.name }}.pem' csr_path: '{{ output_dir }}/{{ item.name }}.csr' privatekey_path: '{{ output_dir }}/{{ item.name }}.key' @@ -51,7 +51,7 @@ when: item.is_ca | default(false) - name: Generate other certificates - openssl_certificate: + x509_certificate: path: '{{ output_dir }}/{{ item.name }}.pem' csr_path: '{{ output_dir }}/{{ item.name }}.csr' provider: ownca @@ -61,7 +61,7 @@ when: not (item.is_ca | default(false)) - name: Get certificate infos - openssl_certificate_info: + x509_certificate_info: path: '{{ output_dir }}/{{ item }}.pem' loop: - cert-1 diff --git a/tests/sanity/ignore-2.9.txt b/tests/sanity/ignore-2.9.txt index c6f6f480..9fab40b2 100644 --- a/tests/sanity/ignore-2.9.txt +++ b/tests/sanity/ignore-2.9.txt @@ -6,9 +6,9 @@ plugins/modules/acme_certificate.py validate-modules:return-syntax-error plugins/modules/certificate_complete_chain.py validate-modules:return-syntax-error plugins/modules/get_certificate.py validate-modules:return-syntax-error plugins/modules/openssh_cert.py validate-modules:return-syntax-error -plugins/modules/openssl_certificate_info.py validate-modules:return-syntax-error plugins/modules/openssl_csr.py validate-modules:return-syntax-error plugins/modules/openssl_csr_info.py validate-modules:return-syntax-error +plugins/modules/x509_certificate_info.py validate-modules:return-syntax-error plugins/modules/x509_crl.py validate-modules:return-syntax-error plugins/modules/x509_crl_info.py validate-modules:return-syntax-error tests/unit/mock/path.py future-import-boilerplate