diff --git a/changelogs/fragments/135-x509_certificate-entrust.yml b/changelogs/fragments/135-x509_certificate-entrust.yml new file mode 100644 index 000000000..763c2afd4 --- /dev/null +++ b/changelogs/fragments/135-x509_certificate-entrust.yml @@ -0,0 +1,2 @@ +bugfixes: +- "x509_certificate - fix ``entrust`` provider, which was broken since community.crypto 0.1.0 due to a feature added before the collection move (https://github.com/ansible-collections/community.crypto/pull/135)." diff --git a/changelogs/fragments/privatekey-csr-certificate-refactoring.yml b/changelogs/fragments/privatekey-csr-certificate-refactoring.yml new file mode 100644 index 000000000..6f1e3e422 --- /dev/null +++ b/changelogs/fragments/privatekey-csr-certificate-refactoring.yml @@ -0,0 +1,4 @@ +minor_changes: +- "openssl_privatekey - refactor module to allow code re-use by openssl_privatekey_pipe (https://github.com/ansible-collections/community.crypto/pull/119)." +- "openssl_csr - refactor module to allow code re-use by openssl_csr_pipe (https://github.com/ansible-collections/community.crypto/pull/123)." +- "x509_certificate - refactor module to allow code re-use by x509_certificate_pipe (https://github.com/ansible-collections/community.crypto/pull/135)." diff --git a/plugins/doc_fragments/module_certificate.py b/plugins/doc_fragments/module_certificate.py new file mode 100644 index 000000000..2c565e65e --- /dev/null +++ b/plugins/doc_fragments/module_certificate.py @@ -0,0 +1,587 @@ +# -*- 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 + + +class ModuleDocFragment(object): + + # Standard files documentation fragment + DOCUMENTATION = r''' +description: + - This module allows one to (re)generate 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 community.crypto 2.0.0. +requirements: + - PyOpenSSL >= 0.15 or cryptography >= 1.6 (if using C(selfsigned), C(ownca) or C(assertonly) provider) +options: + 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 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 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 + + 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 community.crypto 2.0.0. + From that point on, only the C(cryptography) backend will be available. + type: str + default: auto + choices: [ auto, cryptography, pyopenssl ] + +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(community.crypto.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: community.crypto.openssl_csr +- module: community.crypto.openssl_csr_pipe +- module: community.crypto.openssl_dhparam +- module: community.crypto.openssl_pkcs12 +- module: community.crypto.openssl_privatekey +- module: community.crypto.openssl_privatekey_pipe +- module: community.crypto.openssl_publickey +''' + + BACKEND_ACME_DOCUMENTATION = r''' +description: + - This module allows one to (re)generate OpenSSL certificates. +requirements: + - acme-tiny >= 4.0.0 (if using the C(acme) provider) +options: + 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 +''' + + BACKEND_ASSERTONLY_DOCUMENTATION = r''' +description: + - 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 community.crypto 2.0.0. See the examples on how + to emulate C(assertonly) usage with M(community.crypto.x509_certificate_info), + M(community.crypto.openssl_csr_info), M(community.crypto.openssl_privatekey_info) and + M(ansible.builtin.assert). This also allows more flexible checks than + the ones offered by the C(assertonly) provider. + - 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). +options: + csr_path: + description: + - This is not required for the C(assertonly) provider. + + csr_content: + description: + - This is not required for the C(assertonly) provider. + + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + 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 community.crypto 2.0.0. + For alternatives, see the example on replacing C(assertonly). + type: bool + default: no + aliases: [ subjectAltName_strict ] +''' + + BACKEND_ENTRUST_DOCUMENTATION = r''' +options: + 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 +''' + + BACKEND_OWNCA_DOCUMENTATION = r''' +description: + - The C(ownca) provider is intended for generating an OpenSSL certificate signed with your own + CA (Certificate Authority) certificate (self-signed certificate). +options: + 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: + - Content of 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. + - On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer. + Please see U(https://support.apple.com/en-us/HT210176) for more details. + 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 +''' + + BACKEND_SELFSIGNED_DOCUMENTATION = r''' +notes: + - For the C(selfsigned) provider, I(csr_path) and I(csr_content) are optional. If not provided, a + certificate without any information (Subject, Subject Alternative Names, Key Usage, etc.) is created. + +options: + # NOTE: descriptions in options are overwritten, not appended. For that reason, the texts provided + # here for csr_path and csr_content are not visible to the user. That's why this information is + # added to the notes (see above). + + # csr_path: + # description: + # - This is optional for the C(selfsigned) provider. If not provided, a certificate + # without any information (Subject, Subject Alternative Names, Key Usage, etc.) is + # created. + + # csr_content: + # description: + # - This is optional for the C(selfsigned) provider. If not provided, a certificate + # without any information (Subject, Subject Alternative Names, Key Usage, etc.) is + # created. + + 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. + - On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer. + Please see U(https://support.apple.com/en-us/HT210176) for more details. + 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 +''' diff --git a/plugins/doc_fragments/module_csr.py b/plugins/doc_fragments/module_csr.py index fac6e130f..249ec1672 100644 --- a/plugins/doc_fragments/module_csr.py +++ b/plugins/doc_fragments/module_csr.py @@ -266,9 +266,11 @@ class ModuleDocFragment(object): OCSP Must Staple is as requested, and if the request was signed by the given private key. seealso: - module: community.crypto.x509_certificate +- module: community.crypto.x509_certificate_pipe - module: community.crypto.openssl_dhparam - module: community.crypto.openssl_pkcs12 - module: community.crypto.openssl_privatekey +- module: community.crypto.openssl_privatekey_pipe - module: community.crypto.openssl_publickey - module: community.crypto.openssl_csr_info ''' diff --git a/plugins/doc_fragments/module_privatekey.py b/plugins/doc_fragments/module_privatekey.py index 2ac03c27e..e8141631a 100644 --- a/plugins/doc_fragments/module_privatekey.py +++ b/plugins/doc_fragments/module_privatekey.py @@ -154,7 +154,9 @@ class ModuleDocFragment(object): default: full_idempotence seealso: - module: community.crypto.x509_certificate +- module: community.crypto.x509_certificate_pipe - module: community.crypto.openssl_csr +- module: community.crypto.openssl_csr_pipe - module: community.crypto.openssl_dhparam - module: community.crypto.openssl_pkcs12 - module: community.crypto.openssl_publickey diff --git a/plugins/module_utils/crypto/module_backends/certificate.py b/plugins/module_utils/crypto/module_backends/certificate.py new file mode 100644 index 000000000..e82cf8866 --- /dev/null +++ b/plugins/module_utils/crypto/module_backends/certificate.py @@ -0,0 +1,392 @@ +# -*- 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 + + +import abc +import traceback + +from distutils.version import LooseVersion + +from ansible.module_utils import six +from ansible.module_utils.basic import missing_required_lib + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_privatekey, + load_certificate, + load_certificate_request, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_compare_public_keys, +) + +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 +CRYPTOGRAPHY_VERSION = None +try: + import cryptography + from cryptography import x509 + CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__) +except ImportError: + CRYPTOGRAPHY_IMP_ERR = traceback.format_exc() + CRYPTOGRAPHY_FOUND = False +else: + CRYPTOGRAPHY_FOUND = True + + +class CertificateError(OpenSSLObjectError): + pass + + +@six.add_metaclass(abc.ABCMeta) +class CertificateBackend(object): + def __init__(self, module, backend): + self.module = module + self.backend = backend + + self.force = module.params['force'] + 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') + + # 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.privatekey = None + self.csr = None + self.cert = None + self.existing_certificate = None + self.existing_certificate_bytes = None + + self.check_csr_subject = True + self.check_csr_extensions = True + + @abc.abstractmethod + def generate_certificate(self): + """(Re-)Generate certificate.""" + pass + + @abc.abstractmethod + def get_certificate_data(self): + """Return bytes for self.cert.""" + pass + + def set_existing(self, certificate_bytes): + """Set existing certificate bytes. None indicates that the key does not exist.""" + self.existing_certificate_bytes = certificate_bytes + + def has_existing(self): + """Query whether an existing certificate is/has been there.""" + return self.existing_certificate_bytes is not None + + def _ensure_private_key_loaded(self): + """Load the provided private key into self.privatekey.""" + if self.privatekey is not None: + return + if self.privatekey_path is None and self.privatekey_content is None: + return + try: + self.privatekey = load_privatekey( + path=self.privatekey_path, + content=self.privatekey_content, + passphrase=self.privatekey_passphrase, + backend=self.backend, + ) + except OpenSSLBadPassphraseError as exc: + raise CertificateError(exc) + + def _ensure_csr_loaded(self): + """Load the CSR into self.csr.""" + if self.csr is not None: + return + if self.csr_path is None and self.csr_content is None: + return + self.csr = load_certificate_request( + path=self.csr_path, + content=self.csr_content, + backend=self.backend, + ) + + def _ensure_existing_certificate_loaded(self): + """Load the existing certificate into self.existing_certificate.""" + if self.existing_certificate is not None: + return + if self.existing_certificate_bytes is None: + return + self.existing_certificate = load_certificate( + path=None, + content=self.existing_certificate_bytes, + backend=self.backend, + ) + + def _check_privatekey(self): + """Check whether provided parameters match, assuming self.existing_certificate and self.privatekey have been populated.""" + if self.backend == 'pyopenssl': + ctx = OpenSSL.SSL.Context(OpenSSL.SSL.TLSv1_2_METHOD) + ctx.use_privatekey(self.privatekey) + ctx.use_certificate(self.existing_certificate) + try: + ctx.check_privatekey() + return True + except OpenSSL.SSL.Error: + return False + elif self.backend == 'cryptography': + return cryptography_compare_public_keys(self.existing_certificate.public_key(), self.privatekey.public_key()) + + def _check_csr(self): + """Check whether provided parameters match, assuming self.existing_certificate and self.csr have been populated.""" + if self.backend == 'pyopenssl': + # Verify that CSR is signed by certificate's private key + try: + self.csr.verify(self.existing_certificate.get_pubkey()) + except OpenSSL.crypto.Error: + return False + # Check subject + if self.check_csr_subject and self.csr.get_subject() != self.existing_certificate.get_subject(): + return False + # Check extensions + if not self.check_csr_extensions: + return True + csr_extensions = self.csr.get_extensions() + cert_extension_count = self.existing_certificate.get_extension_count() + if len(csr_extensions) != cert_extension_count: + return False + for extension_number in range(0, cert_extension_count): + cert_extension = self.existing_certificate.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 cryptography_compare_public_keys(self.csr.public_key(), self.existing_certificate.public_key()): + return False + # Check subject + if self.check_csr_subject and self.csr.subject != self.existing_certificate.subject: + return False + # Check extensions + if not self.check_csr_extensions: + return True + cert_exts = list(self.existing_certificate.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 _check_subject_key_identifier(self): + """Check whether Subject Key Identifier matches, assuming self.existing_certificate has been populated.""" + if self.backend != 'cryptography': + # We do not support SKI with pyOpenSSL backend + return True + + # Get hold of certificate's SKI + try: + ext = self.existing_certificate.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.existing_certificate.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 + + def needs_regeneration(self): + """Check whether a regeneration is necessary.""" + if self.force or self.existing_certificate_bytes is None: + return True + + try: + self._ensure_existing_certificate_loaded() + except Exception as dummy: + return True + + # Check whether private key matches + self._ensure_private_key_loaded() + if self.privatekey is not None and not self._check_privatekey(): + return True + + # Check whether CSR matches + self._ensure_csr_loaded() + if self.csr is not None and not self._check_csr(): + return True + + # Check SubjectKeyIdentifier + if self.create_subject_key_identifier != 'never_create' and not self._check_subject_key_identifier(): + return True + + return False + + def dump(self, include_certificate): + """Serialize the object into a dictionary.""" + result = { + 'privatekey': self.privatekey_path, + 'csr': self.csr_path + } + if include_certificate: + # Get hold of certificate bytes + certificate_bytes = self.existing_certificate_bytes + if self.cert is not None: + certificate_bytes = self.get_certificate_data() + # Store result + result['certificate'] = certificate_bytes.decode('utf-8') if certificate_bytes else None + return result + + +@six.add_metaclass(abc.ABCMeta) +class CertificateProvider(object): + @abc.abstractmethod + def validate_module_args(self, module): + """Check module arguments""" + + @abc.abstractmethod + def needs_version_two_certs(self, module): + """Whether the provider needs to create a version 2 certificate.""" + + def needs_pyopenssl_get_extensions(self, module): + """Whether the provider needs to use get_extensions() with pyOpenSSL.""" + return True + + @abc.abstractmethod + def create_backend(self, module, backend): + """Create an implementation for a backend. + + Return value must be instance of CertificateBackend. + """ + + +def select_backend(module, backend, provider): + """ + :type module: AnsibleModule + :type backend: str + :type provider: CertificateProvider + """ + provider.validate_module_args(module) + + 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 provider.needs_version_two_certs(module): + 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': + module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated', + version='2.0.0', collection_name='community.crypto') + + if not PYOPENSSL_FOUND: + module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)), + exception=PYOPENSSL_IMP_ERR) + if provider.needs_pyopenssl_get_extensions(module): + try: + getattr(crypto.X509Req, 'get_extensions') + except AttributeError: + module.fail_json(msg='You need to have PyOpenSSL>=0.15') + + 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 provider.needs_version_two_certs(module): + module.fail_json(msg='The cryptography backend does not support v2 certificates, ' + 'use select_crypto_backend=pyopenssl for v2 certificates') + + return provider.create_backend(module, backend) + + +def get_certificate_argument_spec(): + return ArgumentSpec( + argument_spec=dict( + provider=dict(type='str', choices=[]), # choices will be filled by add_XXX_provider_to_argument_spec() in certificate_xxx.py + force=dict(type='bool', default=False,), + csr_path=dict(type='path'), + csr_content=dict(type='str'), + select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']), + + # General properties of a certificate + privatekey_path=dict(type='path'), + privatekey_content=dict(type='str', no_log=True), + privatekey_passphrase=dict(type='str', no_log=True), + ), + mutually_exclusive=[ + ['csr_path', 'csr_content'], + ['privatekey_path', 'privatekey_content'], + ], + ) diff --git a/plugins/module_utils/crypto/module_backends/certificate_acme.py b/plugins/module_utils/crypto/module_backends/certificate_acme.py new file mode 100644 index 000000000..3215b0ec8 --- /dev/null +++ b/plugins/module_utils/crypto/module_backends/certificate_acme.py @@ -0,0 +1,119 @@ +# -*- 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 + + +import os +import tempfile +import traceback + +from ansible.module_utils._text import to_native, to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + CertificateError, + CertificateBackend, + CertificateProvider, +) + + +class AcmeCertificateBackend(CertificateBackend): + def __init__(self, module, backend): + super(AcmeCertificateBackend, 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'] + + if self.csr_content is None and self.csr_path is None: + raise CertificateError( + 'csr_path or csr_content is required for ownca provider' + ) + 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 + ) + + self.acme_tiny_path = self.module.get_bin_path('acme-tiny', required=True) + + def generate_certificate(self): + """(Re-)Generate certificate.""" + + command = [self.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() + self.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 + self.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: + self.cert = to_bytes(self.module.run_command(command, check_rc=True)[1]) + except OSError as exc: + raise CertificateError(exc) + + def get_certificate_data(self): + """Return bytes for self.cert.""" + return self.cert + + def dump(self, include_certificate): + result = super(AcmeCertificateBackend, self).dump(include_certificate) + result['accountkey'] = self.accountkey_path + return result + + +class AcmeCertificateProvider(CertificateProvider): + def validate_module_args(self, module): + 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.') + + def needs_version_two_certs(self, module): + return False + + def create_backend(self, module, backend): + return AcmeCertificateBackend(module, backend) + + +def add_acme_provider_to_argument_spec(argument_spec): + argument_spec.argument_spec['provider']['choices'].append('acme') + argument_spec.argument_spec.update(dict( + 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"), + )) diff --git a/plugins/module_utils/crypto/module_backends/certificate_assertonly.py b/plugins/module_utils/crypto/module_backends/certificate_assertonly.py new file mode 100644 index 000000000..62ef6c211 --- /dev/null +++ b/plugins/module_utils/crypto/module_backends/certificate_assertonly.py @@ -0,0 +1,664 @@ +# -*- 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 + + +import abc +import datetime + +from ansible.module_utils._text import to_native, to_bytes, to_text + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + parse_name_field, + get_relative_time_option, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_compare_public_keys, + cryptography_get_name, + cryptography_name_to_oid, + cryptography_parse_key_usage_params, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import ( + pyopenssl_normalize_name_attribute, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + CertificateBackend, + CertificateProvider, +) + +try: + import OpenSSL + from OpenSSL import crypto +except ImportError: + pass + +try: + import cryptography + from cryptography import x509 + from cryptography.x509 import NameAttribute, Name +except ImportError: + pass + + +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 AssertOnlyCertificateBackend(CertificateBackend): + def __init__(self, module, backend): + super(AssertOnlyCertificateBackend, self).__init__(module, backend) + + self.signature_algorithms = module.params['signature_algorithms'] + if module.params['subject']: + self.subject = parse_name_field(module.params['subject']) + else: + self.subject = [] + self.subject_strict = module.params['subject_strict'] + if module.params['issuer']: + self.issuer = 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._ensure_private_key_loaded() + self._ensure_csr_loaded() + + @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): + 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 != 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 != 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 needs_regeneration(self): + self._ensure_existing_certificate_loaded() + if self.existing_certificate is None: + self.messages = ['Certificate not provided'] + else: + self.messages = self.assertonly() + + return len(self.messages) != 0 + + def generate_certificate(self): + self.module.fail_json(msg=' | '.join(self.messages)) + + def get_certificate_data(self): + return self.existing_certificate_bytes + + +class AssertOnlyCertificateBackendCryptography(AssertOnlyCertificateBackend): + """Validate the supplied cert, using the cryptography backend""" + def __init__(self, module): + super(AssertOnlyCertificateBackendCryptography, self).__init__(module, 'cryptography') + + def _validate_privatekey(self): + return cryptography_compare_public_keys(self.existing_certificate.public_key(), self.privatekey.public_key()) + + def _validate_csr_signature(self): + if not self.csr.is_signature_valid: + return False + return cryptography_compare_public_keys(self.csr.public_key(), self.existing_certificate.public_key()) + + def _validate_csr_subject(self): + return self.csr.subject == self.existing_certificate.subject + + def _validate_csr_extensions(self): + cert_exts = self.existing_certificate.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.existing_certificate.signature_algorithm_oid._name not in self.signature_algorithms: + return self.existing_certificate.signature_algorithm_oid._name + + def _validate_subject(self): + expected_subject = Name([NameAttribute(oid=cryptography_name_to_oid(sub[0]), value=to_text(sub[1])) + for sub in self.subject]) + cert_subject = self.existing_certificate.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=cryptography_name_to_oid(iss[0]), value=to_text(iss[1])) + for iss in self.issuer]) + cert_issuer = self.existing_certificate.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.existing_certificate.not_valid_after + cert_expired = cert_not_after < datetime.datetime.utcnow() + return cert_expired + + def _validate_version(self): + if self.existing_certificate.version == x509.Version.v1: + return 1 + if self.existing_certificate.version == x509.Version.v3: + return 3 + return "unknown" + + def _validate_key_usage(self): + try: + current_key_usage = self.existing_certificate.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 = 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.existing_certificate.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value + usages = [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.existing_certificate.extensions.get_extension_for_class(x509.SubjectAlternativeName).value + expected_san = [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.existing_certificate.not_valid_before + + def _validate_not_after(self): + return self.existing_certificate.not_valid_after + + def _validate_valid_at(self): + rt = get_relative_time_option(self.valid_at, 'valid_at', backend=self.backend) + return self.existing_certificate.not_valid_before, rt, self.existing_certificate.not_valid_after + + def _validate_invalid_at(self): + rt = get_relative_time_option(self.invalid_at, 'invalid_at', backend=self.backend) + return self.existing_certificate.not_valid_before, rt, self.existing_certificate.not_valid_after + + def _validate_valid_in(self): + valid_in_date = get_relative_time_option(self.valid_in, "valid_in", backend=self.backend) + return self.existing_certificate.not_valid_before, valid_in_date, self.existing_certificate.not_valid_after + + +class AssertOnlyCertificateBackendPyOpenSSL(AssertOnlyCertificateBackend): + """validate the supplied certificate.""" + + def __init__(self, module): + super(AssertOnlyCertificateBackendPyOpenSSL, 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.existing_certificate) + try: + ctx.check_privatekey() + return True + except OpenSSL.SSL.Error: + return False + + def _validate_csr_signature(self): + try: + self.csr.verify(self.existing_certificate.get_pubkey()) + except OpenSSL.crypto.Error: + return False + + def _validate_csr_subject(self): + if self.csr.get_subject() != self.existing_certificate.get_subject(): + return False + + def _validate_csr_extensions(self): + csr_extensions = self.csr.get_extensions() + cert_extension_count = self.existing_certificate.get_extension_count() + if len(csr_extensions) != cert_extension_count: + return False + for extension_number in range(0, cert_extension_count): + cert_extension = self.existing_certificate.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.existing_certificate.get_signature_algorithm() not in self.signature_algorithms: + return self.existing_certificate.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.existing_certificate.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.existing_certificate.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.existing_certificate.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.existing_certificate.get_version() + 1 + + def _validate_key_usage(self): + found = False + for extension_idx in range(0, self.existing_certificate.get_extension_count()): + extension = self.existing_certificate.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.existing_certificate.get_extension_count()): + extension = self.existing_certificate.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 _validate_subject_alt_name(self): + found = False + for extension_idx in range(0, self.existing_certificate.get_extension_count()): + extension = self.existing_certificate.get_extension(extension_idx) + if extension.get_short_name() == b'subjectAltName': + found = True + l_altnames = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in + to_text(extension, errors='surrogate_or_strict').split(', ')] + sans = [pyopenssl_normalize_name_attribute(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.existing_certificate.get_notBefore() + + def _validate_not_after(self): + return self.existing_certificate.get_notAfter() + + def _validate_valid_at(self): + rt = get_relative_time_option(self.valid_at, "valid_at", backend=self.backend) + rt = to_bytes(rt, errors='surrogate_or_strict') + return self.existing_certificate.get_notBefore(), rt, self.existing_certificate.get_notAfter() + + def _validate_invalid_at(self): + rt = get_relative_time_option(self.invalid_at, "invalid_at", backend=self.backend) + rt = to_bytes(rt, errors='surrogate_or_strict') + return self.existing_certificate.get_notBefore(), rt, self.existing_certificate.get_notAfter() + + def _validate_valid_in(self): + valid_in_asn1 = 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.existing_certificate.get_notBefore(), valid_in_date, self.existing_certificate.get_notAfter() + + +class AssertOnlyCertificateProvider(CertificateProvider): + def validate_module_args(self, module): + 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.0.0', collection_name='community.crypto') + + def needs_version_two_certs(self, module): + return False + + def create_backend(self, module, backend): + if backend == 'cryptography': + return AssertOnlyCertificateBackendCryptography(module) + if backend == 'pyopenssl': + return AssertOnlyCertificateBackendPyOpenSSL(module) + + +def add_assertonly_provider_to_argument_spec(argument_spec): + argument_spec.argument_spec['provider']['choices'].append('assertonly') + argument_spec.argument_spec.update(dict( + signature_algorithms=dict(type='list', elements='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'), + subject=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'), + subject_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'), + issuer=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'), + issuer_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'), + has_expired=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'), + version=dict(type='int', removed_in_version='2.0.0', removed_from_collection='community.crypto'), + key_usage=dict(type='list', elements='str', aliases=['keyUsage'], + removed_in_version='2.0.0', removed_from_collection='community.crypto'), + key_usage_strict=dict(type='bool', default=False, aliases=['keyUsage_strict'], + removed_in_version='2.0.0', removed_from_collection='community.crypto'), + extended_key_usage=dict(type='list', elements='str', aliases=['extendedKeyUsage'], + removed_in_version='2.0.0', removed_from_collection='community.crypto'), + extended_key_usage_strict=dict(type='bool', default=False, aliases=['extendedKeyUsage_strict'], + removed_in_version='2.0.0', removed_from_collection='community.crypto'), + subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName'], + removed_in_version='2.0.0', removed_from_collection='community.crypto'), + subject_alt_name_strict=dict(type='bool', default=False, aliases=['subjectAltName_strict'], + removed_in_version='2.0.0', removed_from_collection='community.crypto'), + not_before=dict(type='str', aliases=['notBefore'], removed_in_version='2.0.0', removed_from_collection='community.crypto'), + not_after=dict(type='str', aliases=['notAfter'], removed_in_version='2.0.0', removed_from_collection='community.crypto'), + valid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'), + invalid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'), + valid_in=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'), + )) diff --git a/plugins/module_utils/crypto/module_backends/certificate_entrust.py b/plugins/module_utils/crypto/module_backends/certificate_entrust.py new file mode 100644 index 000000000..0e25ce304 --- /dev/null +++ b/plugins/module_utils/crypto/module_backends/certificate_entrust.py @@ -0,0 +1,225 @@ +# -*- 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 + + +import datetime +import time +import os + +from ansible.module_utils._text import to_native, to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.ecs.api import ECSClient, RestOperationException, SessionConfigurationException + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_certificate, + get_relative_time_option, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_serial_number_of_cert, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + CertificateError, + CertificateBackend, + CertificateProvider, +) + +try: + from cryptography.x509.oid import NameOID +except ImportError: + pass + + +class EntrustCertificateBackend(CertificateBackend): + def __init__(self, module, backend): + super(EntrustCertificateBackend, self).__init__(module, backend) + self.trackingId = None + self.notAfter = get_relative_time_option(module.params['entrust_not_after'], 'entrust_not_after', backend=self.backend) + + if self.csr_content is None and self.csr_path is None: + raise CertificateError( + 'csr_path or csr_content is required for entrust provider' + ) + 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) + ) + + self._ensure_csr_loaded() + + # 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: + self.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: + self.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=self.module.params['entrust_api_user'], + entrust_api_key=self.module.params['entrust_api_key'], + entrust_api_cert=self.module.params['entrust_api_client_cert_path'], + entrust_api_cert_key=self.module.params['entrust_api_client_cert_key_path'], + entrust_api_specification_path=self.module.params['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_certificate(self): + """(Re-)Generate certificate.""" + body = {} + + # Read the CSR that was generated for us + if self.csr_content is not None: + # csr_content contains bytes + body['csr'] = to_native(self.csr_content) + else: + with open(self.csr_path, 'r') as csr_file: + body['csr'] = csr_file.read() + + body['certType'] = self.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': self.module.params['entrust_requester_name'], + 'requesterEmail': self.module.params['entrust_requester_email'], + 'requesterPhone': self.module.params['entrust_requester_phone'], + } + + try: + result = self.ecs_client.NewCertRequest(Body=body) + self.trackingId = result.get('trackingId') + except RestOperationException as e: + self.module.fail_json(msg='Failed to request new certificate from Entrust Certificate Services (ECS): {0}'.format(to_native(e.message))) + + self.cert_bytes = to_bytes(result.get('endEntityCert')) + self.cert = load_certificate(path=None, content=self.cert_bytes, backend=self.backend) + + def get_certificate_data(self): + """Return bytes for self.cert.""" + return self.cert_bytes + + def needs_regeneration(self): + parent_check = super(EntrustCertificateBackend, self).needs_regeneration() + + try: + cert_details = self._get_cert_details() + except RestOperationException as e: + self.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 True + + # 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 self.module.params['entrust_cert_type'] and cert_details.get('certType') and self.module.params['entrust_cert_type'] != cert_details.get('certType'): + return True + + return parent_check + + def _get_cert_details(self): + cert_details = {} + try: + self._ensure_existing_certificate_loaded() + except Exception as dummy: + return + if self.existing_certificate: + serial_number = None + expiry = None + if self.backend == 'pyopenssl': + serial_number = "{0:X}".format(self.existing_certificate.get_serial_number()) + time_string = to_native(self.existing_certificate.get_notAfter()) + expiry = datetime.datetime.strptime(time_string, "%Y%m%d%H%M%SZ") + elif self.backend == 'cryptography': + serial_number = "{0:X}".format(cryptography_serial_number_of_cert(self.existing_certificate)) + expiry = self.existing_certificate.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 + + +class EntrustCertificateProvider(CertificateProvider): + def validate_module_args(self, module): + pass + + def needs_version_two_certs(self, module): + return False + + def create_backend(self, module, backend): + return EntrustCertificateBackend(module, backend) + + +def add_entrust_provider_to_argument_spec(argument_spec): + argument_spec.argument_spec['provider']['choices'].append('entrust') + argument_spec.argument_spec.update(dict( + 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'), + )) + argument_spec.required_if.append( + ['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']] + ) diff --git a/plugins/module_utils/crypto/module_backends/certificate_ownca.py b/plugins/module_utils/crypto/module_backends/certificate_ownca.py new file mode 100644 index 000000000..a2e68bbd2 --- /dev/null +++ b/plugins/module_utils/crypto/module_backends/certificate_ownca.py @@ -0,0 +1,363 @@ +# -*- 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 + + +import os + +from distutils.version import LooseVersion +from random import randrange + +from ansible.module_utils._text import to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLBadPassphraseError, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + load_privatekey, + load_certificate, + get_relative_time_option, + select_message_digest, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_key_needs_digest_for_signing, + cryptography_serial_number_of_cert, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + CRYPTOGRAPHY_VERSION, + CertificateError, + CertificateBackend, + CertificateProvider, +) + +try: + from OpenSSL import crypto +except ImportError: + pass + +try: + import cryptography + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.serialization import Encoding +except ImportError: + pass + + +class OwnCACertificateBackendCryptography(CertificateBackend): + def __init__(self, module): + super(OwnCACertificateBackendCryptography, 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 = get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) + self.notAfter = get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) + self.digest = 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 self.csr_path is None: + raise CertificateError( + 'csr_path or csr_content is required for ownca provider' + ) + 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._ensure_csr_loaded() + self.ca_cert = load_certificate( + path=self.ca_cert_path, + content=self.ca_cert_content, + backend=self.backend + ) + try: + self.ca_private_key = load_privatekey( + path=self.ca_privatekey_path, + content=self.ca_privatekey_content, + passphrase=self.ca_privatekey_passphrase, + backend=self.backend + ) + except OpenSSLBadPassphraseError as exc: + module.fail_json(msg=str(exc)) + + if 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_certificate(self): + """(Re-)Generate certificate.""" + 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: + self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') + raise + + self.cert = certificate + + def get_certificate_data(self): + """Return bytes for self.cert.""" + return self.cert.public_bytes(Encoding.PEM) + + def needs_regeneration(self): + if super(OwnCACertificateBackendCryptography, self).needs_regeneration(): + return True + + # 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()) + + self._ensure_existing_certificate_loaded() + try: + ext = self.existing_certificate.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier) + if ext.value != expected_ext: + return True + except cryptography.x509.ExtensionNotFound as dummy: + return True + + return False + + def dump(self, include_certificate): + result = super(OwnCACertificateBackendCryptography, self).dump(include_certificate) + result.update({ + 'ca_cert': self.ca_cert_path, + 'ca_privatekey': self.ca_privatekey_path, + }) + + if self.module.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: + if self.cert is None: + self.cert = self.existing_certificate + 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': cryptography_serial_number_of_cert(self.cert), + }) + + return result + + +def generate_serial_number(): + """Generate a serial number for a certificate""" + while True: + result = randrange(0, 1 << 160) + if result >= 1000: + return result + + +class OwnCACertificateBackendPyOpenSSL(CertificateBackend): + def __init__(self, module): + super(OwnCACertificateBackendPyOpenSSL, self).__init__(module, 'pyopenssl') + + self.notBefore = get_relative_time_option(self.module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) + self.notAfter = get_relative_time_option(self.module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) + self.digest = self.module.params['ownca_digest'] + self.version = self.module.params['ownca_version'] + self.serial_number = generate_serial_number() + if self.module.params['ownca_create_subject_key_identifier'] != 'create_if_not_provided': + self.module.fail_json(msg='ownca_create_subject_key_identifier cannot be used with the pyOpenSSL backend!') + if self.module.params['ownca_create_authority_key_identifier']: + self.module.warn('ownca_create_authority_key_identifier is ignored by the pyOpenSSL backend!') + self.ca_cert_path = self.module.params['ownca_path'] + self.ca_cert_content = self.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 = self.module.params['ownca_privatekey_path'] + self.ca_privatekey_content = self.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 = self.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._ensure_csr_loaded() + self.ca_cert = load_certificate( + path=self.ca_cert_path, + content=self.ca_cert_content, + ) + try: + self.ca_privatekey = load_privatekey( + path=self.ca_privatekey_path, + content=self.ca_privatekey_content, + passphrase=self.ca_privatekey_passphrase + ) + except OpenSSLBadPassphraseError as exc: + self.module.fail_json(msg=str(exc)) + + def generate_certificate(self): + """(Re-)Generate certificate.""" + 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 + + def get_certificate_data(self): + """Return bytes for self.cert.""" + return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert) + + def dump(self, include_certificate): + result = super(OwnCACertificateBackendPyOpenSSL, self).dump(include_certificate) + result.update({ + 'ca_cert': self.ca_cert_path, + 'ca_privatekey': self.ca_privatekey_path, + }) + + if self.module.check_mode: + result.update({ + 'notBefore': self.notBefore, + 'notAfter': self.notAfter, + 'serial_number': self.serial_number, + }) + else: + if self.cert is None: + self.cert = self.existing_certificate + result.update({ + 'notBefore': self.cert.get_notBefore(), + 'notAfter': self.cert.get_notAfter(), + 'serial_number': self.cert.get_serial_number(), + }) + + return result + + +class OwnCACertificateProvider(CertificateProvider): + def validate_module_args(self, module): + 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.') + + def needs_version_two_certs(self, module): + return module.params['ownca_version'] == 2 + + def create_backend(self, module, backend): + if backend == 'cryptography': + return OwnCACertificateBackendCryptography(module) + if backend == 'pyopenssl': + return OwnCACertificateBackendPyOpenSSL(module) + + +def add_ownca_provider_to_argument_spec(argument_spec): + argument_spec.argument_spec['provider']['choices'].append('ownca') + argument_spec.argument_spec.update(dict( + ownca_path=dict(type='path'), + ownca_content=dict(type='str'), + ownca_privatekey_path=dict(type='path'), + ownca_privatekey_content=dict(type='str', no_log=True), + 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), + )) + argument_spec.mutually_exclusive.extend([ + ['ownca_path', 'ownca_content'], + ['ownca_privatekey_path', 'ownca_privatekey_content'], + ]) diff --git a/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py b/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py new file mode 100644 index 000000000..ae58ca271 --- /dev/null +++ b/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py @@ -0,0 +1,263 @@ +# -*- 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 + + +import os + +from random import randrange + +from ansible.module_utils._text import to_bytes + +from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( + get_relative_time_option, + select_message_digest, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( + cryptography_key_needs_digest_for_signing, + cryptography_serial_number_of_cert, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + CertificateError, + CertificateBackend, + CertificateProvider, +) + +try: + from OpenSSL import crypto +except ImportError: + pass + +try: + import cryptography + from cryptography import x509 + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives.serialization import Encoding +except ImportError: + pass + + +class SelfSignedCertificateBackendCryptography(CertificateBackend): + def __init__(self, module): + super(SelfSignedCertificateBackendCryptography, self).__init__(module, 'cryptography') + + self.create_subject_key_identifier = module.params['selfsigned_create_subject_key_identifier'] + self.notBefore = get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) + self.notAfter = get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend) + self.digest = select_message_digest(module.params['selfsigned_digest']) + self.version = module.params['selfsigned_version'] + self.serial_number = x509.random_serial_number() + + if self.csr_path is not 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._module = module + + self._ensure_private_key_loaded() + + self._ensure_csr_loaded() + if self.csr is None: + # Create empty CSR on the fly + csr = cryptography.x509.CertificateSigningRequestBuilder() + csr = csr.subject_name(cryptography.x509.Name([])) + digest = None + if cryptography_key_needs_digest_for_signing(self.privatekey): + digest = self.digest + if digest is None: + self.module.fail_json(msg='Unsupported digest "{0}"'.format(module.params['selfsigned_digest'])) + try: + self.csr = csr.sign(self.privatekey, digest, default_backend()) + except TypeError as e: + if str(e) == 'Algorithm must be a registered hash algorithm.' and digest is None: + self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') + raise + + if 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_certificate(self): + """(Re-)Generate certificate.""" + 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: + self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') + raise + + self.cert = certificate + + def get_certificate_data(self): + """Return bytes for self.cert.""" + return self.cert.public_bytes(Encoding.PEM) + + def dump(self, include_certificate): + result = super(SelfSignedCertificateBackendCryptography, self).dump(include_certificate) + + if self.module.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: + if self.cert is None: + self.cert = self.existing_certificate + 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': cryptography_serial_number_of_cert(self.cert), + }) + + return result + + +def generate_serial_number(): + """Generate a serial number for a certificate""" + while True: + result = randrange(0, 1 << 160) + if result >= 1000: + return result + + +class SelfSignedCertificateBackendPyOpenSSL(CertificateBackend): + def __init__(self, module): + super(SelfSignedCertificateBackendPyOpenSSL, 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 = get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) + self.notAfter = 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 = generate_serial_number() + + if self.csr_path is not 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._ensure_private_key_loaded() + + self._ensure_csr_loaded() + if self.csr is None: + # Create empty CSR on the fly + self.csr = crypto.X509Req() + self.csr.set_pubkey(self.privatekey) + self.csr.sign(self.privatekey, self.digest) + + def generate_certificate(self): + """(Re-)Generate certificate.""" + 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 + + def get_certificate_data(self): + """Return bytes for self.cert.""" + return crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert) + + def dump(self, include_certificate): + result = super(SelfSignedCertificateBackendPyOpenSSL, self).dump(include_certificate) + + if self.module.check_mode: + result.update({ + 'notBefore': self.notBefore, + 'notAfter': self.notAfter, + 'serial_number': self.serial_number, + }) + else: + if self.cert is None: + self.cert = self.existing_certificate + result.update({ + 'notBefore': self.cert.get_notBefore(), + 'notAfter': self.cert.get_notAfter(), + 'serial_number': self.cert.get_serial_number(), + }) + + return result + + +class SelfSignedCertificateProvider(CertificateProvider): + def validate_module_args(self, module): + 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.') + + def needs_version_two_certs(self, module): + return module.params['selfsigned_version'] == 2 + + def create_backend(self, module, backend): + if backend == 'cryptography': + return SelfSignedCertificateBackendCryptography(module) + if backend == 'pyopenssl': + return SelfSignedCertificateBackendPyOpenSSL(module) + + +def add_selfsigned_provider_to_argument_spec(argument_spec): + argument_spec.argument_spec['provider']['choices'].append('selfsigned') + argument_spec.argument_spec.update(dict( + 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'] + ), + )) diff --git a/plugins/module_utils/crypto/module_backends/csr.py b/plugins/module_utils/crypto/module_backends/csr.py index 24a46a3c8..457eab7a1 100644 --- a/plugins/module_utils/crypto/module_backends/csr.py +++ b/plugins/module_utils/crypto/module_backends/csr.py @@ -173,7 +173,7 @@ def __init__(self, module, backend): @abc.abstractmethod def generate_csr(self): - """(Re-)Generate private key.""" + """(Re-)Generate CSR.""" pass @abc.abstractmethod @@ -182,11 +182,11 @@ def get_csr_data(self): pass def set_existing(self, csr_bytes): - """Set existing private key bytes. None indicates that the key does not exist.""" + """Set existing CSR bytes. None indicates that the CSR does not exist.""" self.existing_csr_bytes = csr_bytes def has_existing(self): - """Query whether an existing private key is/has been there.""" + """Query whether an existing CSR is/has been there.""" return self.existing_csr_bytes is not None def _ensure_private_key_loaded(self): @@ -253,7 +253,7 @@ def __init__(self, module): super(CertificateSigningRequestPyOpenSSLBackend, self).__init__(module, 'pyopenssl') def generate_csr(self): - """(Re-)Generate private key.""" + """(Re-)Generate CSR.""" self._ensure_private_key_loaded() req = crypto.X509Req() @@ -418,7 +418,7 @@ def __init__(self, module): module.warn('The cryptography backend only supports version 1. (The only valid value according to RFC 2986.)') def generate_csr(self): - """(Re-)Generate private key.""" + """(Re-)Generate CSR.""" self._ensure_private_key_loaded() csr = cryptography.x509.CertificateSigningRequestBuilder() diff --git a/plugins/modules/openssl_csr_info.py b/plugins/modules/openssl_csr_info.py index 2d1137402..0e2a1ed26 100644 --- a/plugins/modules/openssl_csr_info.py +++ b/plugins/modules/openssl_csr_info.py @@ -53,6 +53,7 @@ seealso: - module: community.crypto.openssl_csr +- module: community.crypto.openssl_csr_pipe ''' EXAMPLES = r''' diff --git a/plugins/modules/openssl_csr_pipe.py b/plugins/modules/openssl_csr_pipe.py index d8fc96441..b789c25ac 100644 --- a/plugins/modules/openssl_csr_pipe.py +++ b/plugins/modules/openssl_csr_pipe.py @@ -40,7 +40,7 @@ - debug: var: result.csr -- name: Generate an OpenSSL Certificate Signing Request with an inline key +- name: Generate an OpenSSL Certificate Signing Request with an inline CSR community.crypto.openssl_csr: content: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.csr') }}" privatekey_content: "{{ private_key_content }}" @@ -48,7 +48,7 @@ register: result - name: Store CSR ansible.builtin.copy: - path: /etc/ssl/csr/www.ansible.com.csr + dest: /etc/ssl/csr/www.ansible.com.csr content: "{{ result.csr }}" when: result is changed ''' @@ -111,7 +111,7 @@ sample: ['email:.com'] csr: description: The (current or generated) CSR's content. - returned: if I(state) is C(present) + returned: changed or success type: str ''' diff --git a/plugins/modules/openssl_publickey.py b/plugins/modules/openssl_publickey.py index daa27a8f8..524e870c6 100644 --- a/plugins/modules/openssl_publickey.py +++ b/plugins/modules/openssl_publickey.py @@ -92,10 +92,13 @@ - files seealso: - module: community.crypto.x509_certificate +- module: community.crypto.x509_certificate_pipe - module: community.crypto.openssl_csr +- module: community.crypto.openssl_csr_pipe - module: community.crypto.openssl_dhparam - module: community.crypto.openssl_pkcs12 - module: community.crypto.openssl_privatekey +- module: community.crypto.openssl_privatekey_pipe ''' EXAMPLES = r''' diff --git a/plugins/modules/x509_certificate.py b/plugins/modules/x509_certificate.py index 43d8a1bd5..7fb649e43 100644 --- a/plugins/modules/x509_certificate.py +++ b/plugins/modules/x509_certificate.py @@ -14,28 +14,11 @@ 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 community.crypto 2.0.0. See the examples on how - to emulate C(assertonly) usage with M(community.crypto.x509_certificate_info), - M(community.crypto.openssl_csr_info), M(community.crypto.openssl_privatekey_info) and - M(ansible.builtin.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 community.crypto 2.0.0. - 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(community.crypto.x509_certificate). From Ansible 2.10 on, it can still be used by the @@ -44,9 +27,6 @@ L(collections,https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#using-collections-in-a-playbook) keyword, the new name M(community.crypto.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) @@ -78,419 +58,13 @@ 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) or C(selfsigned) 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) or C(selfsigned) mode. - - This is mutually exclusive with I(csr_path). - type: str - version_added: '1.0.0' - - privatekey_path: - description: - - Path to the private key to use when signing the certificate. - - This is mutually exclusive with I(privatekey_content). - type: path - privatekey_content: - description: - - Path to the private key to use when signing the certificate. - - This is mutually exclusive with I(privatekey_path). - type: str - version_added: '1.0.0' - - 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. - - On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer. - Please see U(https://support.apple.com/en-us/HT210176) for more details. - 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 - version_added: '1.0.0' - - 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 - version_added: '1.0.0' - - 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. - - On macOS 10.15 and onwards, TLS server certificates must have a validity period of 825 days or fewer. - Please see U(https://support.apple.com/en-us/HT210176) for more details. - 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: + return_content: 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. + - If set to C(yes), will return the (current or generated) certificate's content as I(certificate). 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 version_added: '1.0.0' - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 community.crypto 2.0.0. - 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 @@ -501,109 +75,28 @@ 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 + csr_content: + version_added: '1.0.0' + privatekey_content: + version_added: '1.0.0' + acme_directory: + version_added: '1.0.0' + ownca_content: + version_added: '1.0.0' + ownca_privatekey_content: version_added: '1.0.0' -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(community.crypto.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: community.crypto.openssl_csr -- module: community.crypto.openssl_csr_pipe -- module: community.crypto.openssl_dhparam -- module: community.crypto.openssl_pkcs12 -- module: community.crypto.openssl_privatekey -- module: community.crypto.openssl_publickey +- module: community.crypto.x509_certificate_pipe + +extends_documentation_fragment: + - ansible.builtin.files + - community.crypto.module_certificate + - community.crypto.module_certificate.backend_acme_documentation + - community.crypto.module_certificate.backend_assertonly_documentation + - community.crypto.module_certificate.backend_entrust_documentation + - community.crypto.module_certificate.backend_ownca_documentation + - community.crypto.module_certificate.backend_selfsigned_documentation ''' EXAMPLES = r''' @@ -861,21 +354,39 @@ ''' -import abc -import datetime -import time import os -import tempfile -import traceback -from distutils.version import LooseVersion -from random import randrange +from ansible.module_utils._text import to_native -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.crypto.module_backends.certificate import ( + select_backend, + get_certificate_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_acme import ( + AcmeCertificateProvider, + add_acme_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_assertonly import ( + AssertOnlyCertificateProvider, + add_assertonly_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_entrust import ( + EntrustCertificateProvider, + add_entrust_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_ownca import ( + OwnCACertificateProvider, + add_ownca_provider_to_argument_spec, +) -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 +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_selfsigned import ( + SelfSignedCertificateProvider, + add_selfsigned_provider_to_argument_spec, +) from ansible_collections.community.crypto.plugins.module_utils.io import ( load_file_if_exists, @@ -884,247 +395,40 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( OpenSSLObjectError, - OpenSSLBadPassphraseError, ) from ansible_collections.community.crypto.plugins.module_utils.crypto.support import ( OpenSSLObject, - load_privatekey, - load_certificate, - load_certificate_request, - parse_name_field, - get_relative_time_option, - select_message_digest, -) - -from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import ( - cryptography_compare_public_keys, - cryptography_get_name, - cryptography_name_to_oid, - cryptography_key_needs_digest_for_signing, - cryptography_parse_key_usage_params, - cryptography_serial_number_of_cert, ) -from ansible_collections.community.crypto.plugins.module_utils.crypto.pyopenssl_support import ( - pyopenssl_normalize_name_attribute, -) -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(OpenSSLObjectError): - pass - - -class Certificate(OpenSSLObject): - - def __init__(self, module, backend): - super(Certificate, self).__init__( +class CertificateAbsent(OpenSSLObject): + def __init__(self, module): + super(CertificateAbsent, 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 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 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 generate(self, module): + pass 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 = load_certificate(self.path, backend=self.backend) - except Exception as dummy: - return False - - if self.privatekey_path or self.privatekey_content: - try: - self.privatekey = load_privatekey( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase, - backend=self.backend - ) - except OpenSSLBadPassphraseError as exc: - raise CertificateError(exc) - if not self._validate_privatekey(): - return False - - if self.csr_path or self.csr_content: - self.csr = 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 + super(CertificateAbsent, self).remove(module) 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 + 'privatekey': self.module.params['privatekey_path'], + 'csr': self.module.params['csr_path'] } if self.backup_file: result['backup_file'] = self.backup_file @@ -1134,1535 +438,70 @@ def dump(self, check_mode=False): 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 = get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) - self.notAfter = get_relative_time_option(module.params['selfsigned_not_after'], 'selfsigned_not_after', backend=self.backend) - self.digest = select_message_digest(module.params['selfsigned_digest']) - self.version = module.params['selfsigned_version'] - self.serial_number = x509.random_serial_number() - - if self.csr_path is not 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._module = module - - try: - self.privatekey = load_privatekey( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase, - backend=self.backend - ) - except OpenSSLBadPassphraseError as exc: - module.fail_json(msg=to_native(exc)) - - if self.csr_path is not None or self.csr_content is not None: - self.csr = load_certificate_request( - path=self.csr_path, - content=self.csr_content, - backend=self.backend - ) - else: - # Create empty CSR on the fly - csr = cryptography.x509.CertificateSigningRequestBuilder() - csr = csr.subject_name(cryptography.x509.Name([])) - digest = None - if cryptography_key_needs_digest_for_signing(self.privatekey): - digest = self.digest - if digest is None: - self.module.fail_json(msg='Unsupported digest "{0}"'.format(module.params['selfsigned_digest'])) - try: - self.csr = csr.sign(self.privatekey, digest, default_backend()) - except TypeError as e: - if str(e) == 'Algorithm must be a registered hash algorithm.' and digest is None: - self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.') - raise - - if 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 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) - write_file(module, certificate.public_bytes(Encoding.PEM)) - self.changed = True - else: - self.cert = 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 = 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': cryptography_serial_number_of_cert(self.cert), - }) - - return result - - -def generate_serial_number(): - """Generate a serial number for a certificate""" - while True: - result = randrange(0, 1 << 160) - if result >= 1000: - 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 = get_relative_time_option(module.params['selfsigned_not_before'], 'selfsigned_not_before', backend=self.backend) - self.notAfter = 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 = generate_serial_number() - - if self.csr_path is not 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) - ) - - try: - self.privatekey = load_privatekey( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase, - ) - except OpenSSLBadPassphraseError as exc: - module.fail_json(msg=str(exc)) - - if self.csr_path is not None or self.csr_content is not None: - self.csr = load_certificate_request( - path=self.csr_path, - content=self.csr_content, - ) - else: - # Create empty CSR on the fly - self.csr = crypto.X509Req() - self.csr.set_pubkey(self.privatekey) - self.csr.sign(self.privatekey, self.digest) - - def generate(self, module): - 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) - 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 = 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 = get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) - self.notAfter = get_relative_time_option(module.params['ownca_not_after'], 'ownca_not_after', backend=self.backend) - self.digest = 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 = load_certificate_request( - path=self.csr_path, - content=self.csr_content, - backend=self.backend - ) - self.ca_cert = load_certificate( - path=self.ca_cert_path, - content=self.ca_cert_content, - backend=self.backend +class GenericCertificate(OpenSSLObject): + """Retrieve a certificate using the given module backend.""" + def __init__(self, module, module_backend): + super(GenericCertificate, self).__init__( + module.params['path'], + module.params['state'], + module.params['force'], + module.check_mode ) - try: - self.ca_private_key = load_privatekey( - path=self.ca_privatekey_path, - content=self.ca_privatekey_content, - passphrase=self.ca_privatekey_passphrase, - backend=self.backend - ) - except OpenSSLBadPassphraseError as exc: - module.fail_json(msg=str(exc)) - - if 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) - write_file(module, certificate.public_bytes(Encoding.PEM)) - self.changed = True - else: - self.cert = 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 = 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': cryptography_serial_number_of_cert(self.cert), - }) - - return result - - -class OwnCACertificate(Certificate): - """Generate the own CA certificate.""" + self.module = module + self.return_content = module.params['return_content'] + self.backup = module.params['backup'] + self.backup_file = None - def __init__(self, module): - super(OwnCACertificate, self).__init__(module, 'pyopenssl') - self.notBefore = get_relative_time_option(module.params['ownca_not_before'], 'ownca_not_before', backend=self.backend) - self.notAfter = 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 = generate_serial_number() - 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 = load_certificate_request( - path=self.csr_path, - content=self.csr_content, - ) - self.ca_cert = load_certificate( - path=self.ca_cert_path, - content=self.ca_cert_content, - ) - try: - self.ca_privatekey = load_privatekey( - path=self.ca_privatekey_path, - content=self.ca_privatekey_content, - passphrase=self.ca_privatekey_passphrase - ) - except OpenSSLBadPassphraseError as exc: - module.fail_json(msg=str(exc)) + self.module_backend = module_backend + self.module_backend.set_existing(load_file_if_exists(self.path, module)) 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) - write_file(module, crypto.dump_certificate(crypto.FILETYPE_PEM, self.cert)) + if self.module_backend.needs_regeneration(): + if not self.check_mode: + self.module_backend.generate_certificate() + result = self.module_backend.get_certificate_data() + if self.backup: + self.backup_file = module.backup_local(self.path) + write_file(module, result) self.changed = True file_args = module.load_file_common_arguments(module.params) - if module.set_fs_attributes_if_different(file_args, False): - self.changed = True - - 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 = 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 = parse_name_field(module.params['subject']) - else: - self.subject = [] - self.subject_strict = module.params['subject_strict'] - if module.params['issuer']: - self.issuer = 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 = load_certificate(self.path, backend=self.backend) - if self.privatekey_path is not None or self.privatekey_content is not None: - try: - self.privatekey = load_privatekey( - path=self.privatekey_path, - content=self.privatekey_content, - passphrase=self.privatekey_passphrase, - backend=self.backend - ) - except OpenSSLBadPassphraseError as exc: - raise CertificateError(exc) - if self.csr_path is not None or self.csr_content is not None: - self.csr = 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 != 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 != 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 = 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 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 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=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=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 = 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 = [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 = [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 = 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 = 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 = 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 _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 = [pyopenssl_normalize_name_attribute(altname.strip()) for altname in - to_text(extension, errors='surrogate_or_strict').split(', ')] - sans = [pyopenssl_normalize_name_attribute(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 = 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 = 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 = 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 = 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 = 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) - write_file(module, to_bytes(result.get('endEntityCert'))) - self.cert = load_certificate(self.path, backend=self.backend) - self.changed = True + self.changed = module.set_fs_attributes_if_different(file_args, self.changed) 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(cryptography_serial_number_of_cert(self.cert)) - 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 + return super(GenericCertificate, self).check(module, perms_required) and not self.module_backend.needs_regeneration() def dump(self, check_mode=False): - - result = { + result = self.module_backend.dump(include_certificate=self.return_content) + result.update({ '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 = 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) - 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 = 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', no_log=True), - privatekey_passphrase=dict(type='str', no_log=True), - - # provider: assertonly - signature_algorithms=dict(type='list', elements='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'), - subject=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'), - subject_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'), - issuer=dict(type='dict', removed_in_version='2.0.0', removed_from_collection='community.crypto'), - issuer_strict=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'), - has_expired=dict(type='bool', default=False, removed_in_version='2.0.0', removed_from_collection='community.crypto'), - version=dict(type='int', removed_in_version='2.0.0', removed_from_collection='community.crypto'), - key_usage=dict(type='list', elements='str', aliases=['keyUsage'], - removed_in_version='2.0.0', removed_from_collection='community.crypto'), - key_usage_strict=dict(type='bool', default=False, aliases=['keyUsage_strict'], - removed_in_version='2.0.0', removed_from_collection='community.crypto'), - extended_key_usage=dict(type='list', elements='str', aliases=['extendedKeyUsage'], - removed_in_version='2.0.0', removed_from_collection='community.crypto'), - extended_key_usage_strict=dict(type='bool', default=False, aliases=['extendedKeyUsage_strict'], - removed_in_version='2.0.0', removed_from_collection='community.crypto'), - subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName'], - removed_in_version='2.0.0', removed_from_collection='community.crypto'), - subject_alt_name_strict=dict(type='bool', default=False, aliases=['subjectAltName_strict'], - removed_in_version='2.0.0', removed_from_collection='community.crypto'), - not_before=dict(type='str', aliases=['notBefore'], removed_in_version='2.0.0', removed_from_collection='community.crypto'), - not_after=dict(type='str', aliases=['notAfter'], removed_in_version='2.0.0', removed_from_collection='community.crypto'), - valid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'), - invalid_at=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'), - valid_in=dict(type='str', removed_in_version='2.0.0', removed_from_collection='community.crypto'), - - # 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', no_log=True), - 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, + argument_spec = get_certificate_argument_spec() + add_acme_provider_to_argument_spec(argument_spec) + add_assertonly_provider_to_argument_spec(argument_spec) + add_entrust_provider_to_argument_spec(argument_spec) + add_ownca_provider_to_argument_spec(argument_spec) + add_selfsigned_provider_to_argument_spec(argument_spec) + argument_spec.argument_spec.update(dict( + state=dict(type='str', default='present', choices=['present', 'absent']), + path=dict(type='path', required=True), + backup=dict(type='bool', default=False), + return_content=dict(type='bool', default=False), + )) + argument_spec.required_if.append(['state', 'present', ['provider']]) + module = argument_spec.create_ansible_module( 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'], - ], + supports_check_mode=True, ) + 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.0.0', collection_name='community.crypto') @@ -2671,10 +510,14 @@ def main(): if module.params['state'] == 'absent': certificate = CertificateAbsent(module) - else: - if module.params['provider'] not in ('assertonly', 'selfsigned') 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 or selfsigned') + 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) + + else: base_dir = os.path.dirname(module.params['path']) or '.' if not os.path.isdir(base_dir): module.fail_json( @@ -2683,101 +526,18 @@ def main(): ) 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.0.0', collection_name='community.crypto') - 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.') + provider_map = { + 'acme': AcmeCertificateProvider, + 'assertonly': AssertOnlyCertificateProvider, + 'entrust': EntrustCertificateProvider, + 'ownca': OwnCACertificateProvider, + 'selfsigned': SelfSignedCertificateProvider, + } 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.0.0', collection_name='community.crypto') - 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) - + module_backend = select_backend(module, backend, provider_map[provider]()) + certificate = GenericCertificate(module, module_backend) 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) diff --git a/plugins/modules/x509_certificate_info.py b/plugins/modules/x509_certificate_info.py index 9f14e3392..9cbd9f22f 100644 --- a/plugins/modules/x509_certificate_info.py +++ b/plugins/modules/x509_certificate_info.py @@ -74,6 +74,7 @@ They are all in UTC. seealso: - module: community.crypto.x509_certificate +- module: community.crypto.x509_certificate_pipe ''' EXAMPLES = r''' diff --git a/plugins/modules/x509_certificate_pipe.py b/plugins/modules/x509_certificate_pipe.py new file mode 100644 index 000000000..04b5ae8ab --- /dev/null +++ b/plugins/modules/x509_certificate_pipe.py @@ -0,0 +1,206 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016-2017, Yanis Guenane +# Copyright: (c) 2017, Markus Teufelberger +# Copyright: (2) 2020, Felix Fontein +# 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 + + +DOCUMENTATION = r''' +--- +module: x509_certificate_pipe +short_description: Generate and/or check OpenSSL certificates +version_added: 1.3.0 +description: + - It implements a notion of provider (ie. C(selfsigned), C(ownca), C(entrust)) + for your certificate. + - "Please note that the module regenerates an 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." +author: + - Yanis Guenane (@Spredzy) + - Markus Teufelberger (@MarkusTeufelberger) + - Felix Fontein (@felixfontein) +options: + provider: + description: + - Name of the provider to use to generate/retrieve the OpenSSL certificate. + - "The C(entrust) provider requires credentials for the + L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API." + type: str + choices: [ entrust, ownca, selfsigned ] + required: true + + content: + description: + - The existing certificate. + type: str + +seealso: +- module: community.crypto.x509_certificate + +extends_documentation_fragment: + - community.crypto.module_certificate + - community.crypto.module_certificate.backend_entrust_documentation + - community.crypto.module_certificate.backend_ownca_documentation + - community.crypto.module_certificate.backend_selfsigned_documentation +''' + +EXAMPLES = r''' +- name: Generate a Self Signed OpenSSL certificate + community.crypto.x509_certificate_pipe: + provider: selfsigned + privatekey_path: /etc/ssl/private/ansible.com.pem + csr_path: /etc/ssl/csr/ansible.com.csr + register: result +- ansible.builtin.debug: + var: result.certificate + +# In the following example, both CSR and certificate file are stored on the +# machine where ansible-playbook is executed, while the OwnCA data (certificate, +# private key) are stored on the remote machine. + +- name: (1/2) Generate an OpenSSL Certificate with the CSR provided inline + community.crypto.x509_certificate_pipe: + provider: ownca + content: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.crt') }}" + csr_content: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.csr') }}" + ownca_cert: /path/to/ca_cert.crt + ownca_privatekey: /path/to/ca_cert.key + ownca_privatekey_passphrase: hunter2 + register: result + +- name: (2/2) Store certificate + ansible.builtin.copy: + dest: /etc/ssl/csr/www.ansible.com.crt + content: "{{ result.certificate }}" + delegate_to: localhost + when: result is changed + +# In the following example, the certificate from another machine is signed by +# our OwnCA whose private key and certificate are only available on this +# machine (where ansible-playbook is executed), without having to write +# the certificate file to disk on localhost. The CSR could have been +# provided by community.crypto.openssl_csr_pipe earlier, or also have been +# read from the remote machine. + +- name: (1/3) Read certificate's contents from remote machine + ansible.builtin.slurp: + src: /etc/ssl/csr/www.ansible.com.crt + register: certificate_content + +- name: (2/3) Generate an OpenSSL Certificate with the CSR provided inline + community.crypto.x509_certificate_pipe: + provider: ownca + content: "{{ certificate_content.content | b64decode }}" + csr_content: "{{ the_csr }}" + ownca_cert: /path/to/ca_cert.crt + ownca_privatekey: /path/to/ca_cert.key + ownca_privatekey_passphrase: hunter2 + delegate_to: localhost + register: result + +- name: (3/3) Store certificate + ansible.builtin.copy: + dest: /etc/ssl/csr/www.ansible.com.crt + content: "{{ result.certificate }}" + when: result is changed +''' + +RETURN = r''' +certificate: + description: The (current or generated) certificate's content. + returned: changed or success + type: str +''' + + +import os + +from ansible.module_utils._text import to_native + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import ( + select_backend, + get_certificate_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_entrust import ( + EntrustCertificateProvider, + add_entrust_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_ownca import ( + OwnCACertificateProvider, + add_ownca_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_selfsigned import ( + SelfSignedCertificateProvider, + add_selfsigned_provider_to_argument_spec, +) + +from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import ( + OpenSSLObjectError, +) + + +class GenericCertificate(object): + """Retrieve a certificate using the given module backend.""" + def __init__(self, module, module_backend): + self.check_mode = module.check_mode + self.module_backend = module_backend + self.changed = False + if module.params['content'] is not None: + self.module_backend.set_existing(module.params['content'].encode('utf-8')) + + def generate(self, module): + if self.module_backend.needs_regeneration(): + if not self.check_mode: + self.module_backend.generate_certificate() + self.changed = True + + def dump(self, check_mode=False): + result = self.module_backend.dump(include_certificate=True) + result.update({ + 'changed': self.changed, + }) + return result + + +def main(): + argument_spec = get_certificate_argument_spec() + argument_spec.argument_spec['provider']['required'] = True + add_entrust_provider_to_argument_spec(argument_spec) + add_ownca_provider_to_argument_spec(argument_spec) + add_selfsigned_provider_to_argument_spec(argument_spec) + argument_spec.argument_spec.update(dict( + content=dict(type='str'), + )) + module = argument_spec.create_ansible_module( + supports_check_mode=True, + ) + + try: + provider = module.params['provider'] + provider_map = { + 'entrust': EntrustCertificateProvider, + 'ownca': OwnCACertificateProvider, + 'selfsigned': SelfSignedCertificateProvider, + } + + backend = module.params['select_crypto_backend'] + module_backend = select_backend(module, backend, provider_map[provider]()) + certificate = GenericCertificate(module, module_backend) + certificate.generate(module) + result = certificate.dump() + module.exit_json(**result) + except OpenSSLObjectError as exc: + module.fail_json(msg=to_native(exc)) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/x509_certificate_pipe/aliases b/tests/integration/targets/x509_certificate_pipe/aliases new file mode 100644 index 000000000..6eae8bd8d --- /dev/null +++ b/tests/integration/targets/x509_certificate_pipe/aliases @@ -0,0 +1,2 @@ +shippable/posix/group1 +destructive diff --git a/tests/integration/targets/x509_certificate_pipe/meta/main.yml b/tests/integration/targets/x509_certificate_pipe/meta/main.yml new file mode 100644 index 000000000..d1a318dba --- /dev/null +++ b/tests/integration/targets/x509_certificate_pipe/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - setup_openssl + - setup_pyopenssl diff --git a/tests/integration/targets/x509_certificate_pipe/tasks/impl.yml b/tests/integration/targets/x509_certificate_pipe/tasks/impl.yml new file mode 100644 index 000000000..5b024ac2b --- /dev/null +++ b/tests/integration/targets/x509_certificate_pipe/tasks/impl.yml @@ -0,0 +1,237 @@ +--- +- name: "({{ select_crypto_backend }}) Generate privatekey" + openssl_privatekey: + path: '{{ output_dir }}/{{ item }}.pem' + size: 2048 + loop: + - privatekey + - privatekey2 + +- name: "({{ select_crypto_backend }}) Generate CSRs" + openssl_csr: + privatekey_path: '{{ output_dir }}/{{ item.key }}.pem' + path: '{{ output_dir }}/{{ item.name }}.csr' + subject: + commonName: '{{ item.cn }}' + select_crypto_backend: '{{ select_crypto_backend }}' + loop: + - name: cert + key: privatekey + cn: www.ansible.com + - name: cert-2 + key: privatekey + cn: ansible.com + - name: cert-3 + key: privatekey2 + cn: example.com + - name: cert-4 + key: privatekey2 + cn: example.org + +## Self Signed + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate (check mode)" + x509_certificate_pipe: + provider: selfsigned + privatekey_path: '{{ output_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + register: generate_certificate_check + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate" + x509_certificate_pipe: + provider: selfsigned + privatekey_path: '{{ output_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_certificate + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate (idempotent)" + x509_certificate_pipe: + provider: selfsigned + content: "{{ generate_certificate.certificate }}" + privatekey_path: '{{ output_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_certificate_idempotent + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate (idempotent, check mode)" + x509_certificate_pipe: + provider: selfsigned + content: "{{ generate_certificate.certificate }}" + privatekey_path: '{{ output_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + register: generate_certificate_idempotent_check + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate (changed)" + x509_certificate_pipe: + provider: selfsigned + content: "{{ generate_certificate.certificate }}" + privatekey_path: '{{ output_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert-2.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: generate_certificate_changed + +- name: "({{ select_crypto_backend }}) Generate self-signed certificate (changed, check mode)" + x509_certificate_pipe: + provider: selfsigned + content: "{{ generate_certificate.certificate }}" + privatekey_path: '{{ output_dir }}/privatekey.pem' + selfsigned_not_before: 20181023133742Z + selfsigned_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert-2.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + register: generate_certificate_changed_check + +- name: "({{ select_crypto_backend }}) Validate certificate (test - privatekey modulus)" + shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ output_dir }}/privatekey.pem' + register: privatekey_modulus + +- name: "({{ select_crypto_backend }}) Validate certificate (test - Common Name)" + shell: "{{ openssl_binary }} x509 -noout -subject -in /dev/stdin -nameopt oneline,-space_eq" + args: + stdin: "{{ generate_certificate.certificate }}" + register: certificate_cn + +- name: "({{ select_crypto_backend }}) Validate certificate (test - certificate modulus)" + shell: '{{ openssl_binary }} x509 -noout -modulus -in /dev/stdin' + args: + stdin: "{{ generate_certificate.certificate }}" + register: certificate_modulus + +- name: "({{ select_crypto_backend }}) Validate certificate (assert)" + assert: + that: + - certificate_cn.stdout.split('=')[-1] == 'www.ansible.com' + - certificate_modulus.stdout == privatekey_modulus.stdout + +- name: "({{ select_crypto_backend }}) Validate certificate (check mode, idempotency)" + assert: + that: + - generate_certificate_check is changed + - generate_certificate is changed + - generate_certificate_idempotent is not changed + - generate_certificate_idempotent_check is not changed + - generate_certificate_changed is changed + - generate_certificate_changed_check is changed + +## Own CA + +- name: "({{ select_crypto_backend }}) Generate own CA certificate (check mode)" + x509_certificate_pipe: + provider: ownca + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert-3.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + register: ownca_generate_certificate_check + +- name: "({{ select_crypto_backend }}) Generate own CA certificate" + x509_certificate_pipe: + provider: ownca + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert-3.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_generate_certificate + +- name: "({{ select_crypto_backend }}) Generate own CA certificate (idempotent)" + x509_certificate_pipe: + provider: ownca + content: "{{ ownca_generate_certificate.certificate }}" + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert-3.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_generate_certificate_idempotent + +- name: "({{ select_crypto_backend }}) Generate own CA certificate (idempotent, check mode)" + x509_certificate_pipe: + provider: ownca + content: "{{ ownca_generate_certificate.certificate }}" + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert-3.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + register: ownca_generate_certificate_idempotent_check + +- name: "({{ select_crypto_backend }}) Generate own CA certificate (changed)" + x509_certificate_pipe: + provider: ownca + content: "{{ ownca_generate_certificate.certificate }}" + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert-4.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + register: ownca_generate_certificate_changed + +- name: "({{ select_crypto_backend }}) Generate own CA certificate (changed, check mode)" + x509_certificate_pipe: + provider: ownca + content: "{{ ownca_generate_certificate.certificate }}" + ownca_content: '{{ generate_certificate.certificate }}' + ownca_privatekey_path: '{{ output_dir }}/privatekey.pem' + ownca_not_before: 20181023133742Z + ownca_not_after: 20191023133742Z + csr_path: '{{ output_dir }}/cert-4.csr' + select_crypto_backend: '{{ select_crypto_backend }}' + check_mode: yes + register: ownca_generate_certificate_changed_check + +- name: "({{ select_crypto_backend }}) Validate certificate (test - privatekey modulus)" + shell: '{{ openssl_binary }} rsa -noout -modulus -in {{ output_dir }}/privatekey2.pem' + register: privatekey_modulus + +- name: "({{ select_crypto_backend }}) Validate certificate (test - Common Name)" + shell: "{{ openssl_binary }} x509 -noout -subject -in /dev/stdin -nameopt oneline,-space_eq" + args: + stdin: "{{ ownca_generate_certificate.certificate }}" + register: certificate_cn + +- name: "({{ select_crypto_backend }}) Validate certificate (test - certificate modulus)" + shell: '{{ openssl_binary }} x509 -noout -modulus -in /dev/stdin' + args: + stdin: "{{ ownca_generate_certificate.certificate }}" + register: certificate_modulus + +- name: "({{ select_crypto_backend }}) Validate certificate (assert)" + assert: + that: + - certificate_cn.stdout.split('=')[-1] == 'example.com' + - certificate_modulus.stdout == privatekey_modulus.stdout + +- name: "({{ select_crypto_backend }}) Validate certificate (check mode, idempotency)" + assert: + that: + - ownca_generate_certificate_check is changed + - ownca_generate_certificate is changed + - ownca_generate_certificate_idempotent is not changed + - ownca_generate_certificate_idempotent_check is not changed + - ownca_generate_certificate_changed is changed + - ownca_generate_certificate_changed_check is changed diff --git a/tests/integration/targets/x509_certificate_pipe/tasks/main.yml b/tests/integration/targets/x509_certificate_pipe/tasks/main.yml new file mode 100644 index 000000000..023443640 --- /dev/null +++ b/tests/integration/targets/x509_certificate_pipe/tasks/main.yml @@ -0,0 +1,39 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Prepare private key for backend autodetection test + openssl_privatekey: + path: '{{ output_dir }}/privatekey_backend_selection.pem' +- name: Run module with backend autodetection + x509_certificate_pipe: + provider: selfsigned + privatekey_path: '{{ output_dir }}/privatekey_backend_selection.pem' + +- block: + - name: Running tests with pyOpenSSL backend + include_tasks: impl.yml + vars: + select_crypto_backend: pyopenssl + + when: pyopenssl_version.stdout is version('0.15', '>=') + +- name: Remove output directory + file: + path: "{{ output_dir }}" + state: absent + +- name: Re-create output directory + file: + path: "{{ output_dir }}" + state: directory + +- block: + - name: Running tests with cryptography backend + include_tasks: impl.yml + vars: + select_crypto_backend: cryptography + + when: cryptography_version.stdout is version('1.6', '>=')