summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/crypto/plugins
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 16:03:42 +0000
commit66cec45960ce1d9c794e9399de15c138acb18aed (patch)
tree59cd19d69e9d56b7989b080da7c20ef1a3fe2a5a /ansible_collections/community/crypto/plugins
parentInitial commit. (diff)
downloadansible-66cec45960ce1d9c794e9399de15c138acb18aed.tar.xz
ansible-66cec45960ce1d9c794e9399de15c138acb18aed.zip
Adding upstream version 7.3.0+dfsg.upstream/7.3.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/community/crypto/plugins')
-rw-r--r--ansible_collections/community/crypto/plugins/action/openssl_privatekey_pipe.py108
-rw-r--r--ansible_collections/community/crypto/plugins/doc_fragments/acme.py139
-rw-r--r--ansible_collections/community/crypto/plugins/doc_fragments/attributes.py85
-rw-r--r--ansible_collections/community/crypto/plugins/doc_fragments/ecs_credential.py44
-rw-r--r--ansible_collections/community/crypto/plugins/doc_fragments/module_certificate.py404
-rw-r--r--ansible_collections/community/crypto/plugins/doc_fragments/module_csr.py325
-rw-r--r--ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey.py151
-rw-r--r--ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey_convert.py48
-rw-r--r--ansible_collections/community/crypto/plugins/doc_fragments/name_encoding.py31
-rw-r--r--ansible_collections/community/crypto/plugins/filter/openssl_csr_info.py313
-rw-r--r--ansible_collections/community/crypto/plugins/filter/openssl_privatekey_info.py193
-rw-r--r--ansible_collections/community/crypto/plugins/filter/openssl_publickey_info.py162
-rw-r--r--ansible_collections/community/crypto/plugins/filter/split_pem.py64
-rw-r--r--ansible_collections/community/crypto/plugins/filter/x509_certificate_info.py346
-rw-r--r--ansible_collections/community/crypto/plugins/filter/x509_crl_info.py196
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/_version.py345
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/acme/account.py252
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/acme/acme.py452
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/acme/backend_cryptography.py393
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/acme/backend_openssl_cli.py302
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/acme/backends.py59
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/acme/certificates.py129
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/acme/challenges.py303
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/acme/errors.py145
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/acme/io.py87
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/acme/orders.py130
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/acme/utils.py72
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/_asn1.py154
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/_obj2txt.py57
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/_objects.py35
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/_objects_data.py1115
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/basic.py159
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_crl.py114
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_support.py809
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/math.py70
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate.py354
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_acme.py120
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_entrust.py211
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_info.py411
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_ownca.py276
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py198
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/common.py35
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/crl_info.py102
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr.py675
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr_info.py334
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey.py533
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_convert.py236
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_info.py287
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/publickey_info.py168
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/openssh.py13
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/pem.py73
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/crypto/support.py401
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/ecs/api.py346
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/io.py104
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/openssh/backends/common.py346
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/openssh/backends/keypair_backend.py480
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/openssh/certificate.py666
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/openssh/cryptography.py685
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/openssh/utils.py392
-rw-r--r--ansible_collections/community/crypto/plugins/module_utils/version.py18
-rw-r--r--ansible_collections/community/crypto/plugins/modules/acme_account.py345
-rw-r--r--ansible_collections/community/crypto/plugins/modules/acme_account_info.py320
-rw-r--r--ansible_collections/community/crypto/plugins/modules/acme_certificate.py919
-rw-r--r--ansible_collections/community/crypto/plugins/modules/acme_certificate_revoke.py245
-rw-r--r--ansible_collections/community/crypto/plugins/modules/acme_challenge_cert_helper.py319
-rw-r--r--ansible_collections/community/crypto/plugins/modules/acme_inspect.py325
-rw-r--r--ansible_collections/community/crypto/plugins/modules/certificate_complete_chain.py375
-rw-r--r--ansible_collections/community/crypto/plugins/modules/crypto_info.py337
-rw-r--r--ansible_collections/community/crypto/plugins/modules/ecs_certificate.py966
-rw-r--r--ansible_collections/community/crypto/plugins/modules/ecs_domain.py412
-rw-r--r--ansible_collections/community/crypto/plugins/modules/get_certificate.py397
-rw-r--r--ansible_collections/community/crypto/plugins/modules/luks_device.py1031
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssh_cert.py578
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssh_keypair.py244
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_csr.py359
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_csr_info.py359
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_csr_pipe.py183
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_dhparam.py431
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_pkcs12.py848
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_privatekey.py290
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_privatekey_convert.py171
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_privatekey_info.py278
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_privatekey_pipe.py131
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_publickey.py488
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_publickey_info.py217
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_signature.py276
-rw-r--r--ansible_collections/community/crypto/plugins/modules/openssl_signature_info.py299
-rw-r--r--ansible_collections/community/crypto/plugins/modules/x509_certificate.py419
-rw-r--r--ansible_collections/community/crypto/plugins/modules/x509_certificate_info.py466
-rw-r--r--ansible_collections/community/crypto/plugins/modules/x509_certificate_pipe.py211
-rw-r--r--ansible_collections/community/crypto/plugins/modules/x509_crl.py914
-rw-r--r--ansible_collections/community/crypto/plugins/modules/x509_crl_info.py220
-rw-r--r--ansible_collections/community/crypto/plugins/plugin_utils/action_module.py765
-rw-r--r--ansible_collections/community/crypto/plugins/plugin_utils/filter_module.py22
94 files changed, 29415 insertions, 0 deletions
diff --git a/ansible_collections/community/crypto/plugins/action/openssl_privatekey_pipe.py b/ansible_collections/community/crypto/plugins/action/openssl_privatekey_pipe.py
new file mode 100644
index 00000000..dc1a1697
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/action/openssl_privatekey_pipe.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import base64
+
+from ansible.module_utils.common.text.converters import to_native, to_bytes
+
+from ansible_collections.community.crypto.plugins.plugin_utils.action_module import ActionModuleBase
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey import (
+ select_backend,
+ get_privatekey_argument_spec,
+)
+
+
+class PrivateKeyModule(object):
+ def __init__(self, module, module_backend):
+ self.module = module
+ self.module_backend = module_backend
+ self.check_mode = module.check_mode
+ self.changed = False
+ self.return_current_key = module.params['return_current_key']
+
+ if module.params['content'] is not None:
+ if module.params['content_base64']:
+ try:
+ data = base64.b64decode(module.params['content'])
+ except Exception as e:
+ module.fail_json(msg='Cannot decode Base64 encoded data: {0}'.format(e))
+ else:
+ data = to_bytes(module.params['content'])
+ module_backend.set_existing(data)
+
+ def generate(self, module):
+ """Generate a keypair."""
+
+ if self.module_backend.needs_regeneration():
+ # Regenerate
+ if not self.check_mode:
+ self.module_backend.generate_private_key()
+ privatekey_data = self.module_backend.get_private_key_data()
+ self.privatekey_bytes = privatekey_data
+ self.changed = True
+ elif self.module_backend.needs_conversion():
+ # Convert
+ if not self.check_mode:
+ self.module_backend.convert_private_key()
+ privatekey_data = self.module_backend.get_private_key_data()
+ self.privatekey_bytes = privatekey_data
+ self.changed = True
+
+ def dump(self):
+ """Serialize the object into a dictionary."""
+ result = self.module_backend.dump(include_key=self.changed or self.return_current_key)
+ result['changed'] = self.changed
+ return result
+
+
+class ActionModule(ActionModuleBase):
+ @staticmethod
+ def setup_module():
+ argument_spec = get_privatekey_argument_spec()
+ argument_spec.argument_spec.update(dict(
+ content=dict(type='str', no_log=True),
+ content_base64=dict(type='bool', default=False),
+ return_current_key=dict(type='bool', default=False),
+ ))
+ return argument_spec, dict(
+ supports_check_mode=True,
+ )
+
+ @staticmethod
+ def run_module(module):
+ backend, module_backend = select_backend(
+ module=module,
+ backend=module.params['select_crypto_backend'],
+ )
+
+ try:
+ private_key = PrivateKeyModule(module, module_backend)
+ private_key.generate(module)
+ result = private_key.dump()
+ if private_key.return_current_key:
+ # In case the module's input (`content`) is returned as `privatekey`:
+ # Since `content` is no_log=True, `privatekey`'s value will get replaced by
+ # VALUE_SPECIFIED_IN_NO_LOG_PARAMETER. To avoid this, we remove the value of
+ # `content` from module.no_log_values. Since we explicitly set
+ # `module.no_log = True`, this should be safe.
+ module.no_log = True
+ try:
+ module.no_log_values.remove(module.params['content'])
+ except KeyError:
+ pass
+ module.params['content'] = 'ANSIBLE_NO_LOG_VALUE'
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/acme.py b/ansible_collections/community/crypto/plugins/doc_fragments/acme.py
new file mode 100644
index 00000000..a50cedd6
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/doc_fragments/acme.py
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+
+ # Standard files documentation fragment
+ DOCUMENTATION = r'''
+notes:
+ - "If a new enough version of the C(cryptography) library
+ is available (see Requirements for details), it will be used
+ instead of the C(openssl) binary. This can be explicitly disabled
+ or enabled with the C(select_crypto_backend) option. Note that using
+ the C(openssl) binary will be slower and less secure, as private key
+ contents always have to be stored on disk (see
+ C(account_key_content))."
+ - "Although the defaults are chosen so that the module can be used with
+ the L(Let's Encrypt,https://letsencrypt.org/) CA, the module can in
+ principle be used with any CA providing an ACME endpoint, such as
+ L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme)."
+ - "So far, the ACME modules have only been tested by the developers against
+ Let's Encrypt (staging and production), Buypass (staging and production), ZeroSSL (production),
+ and L(Pebble testing server,https://github.com/letsencrypt/Pebble). We have got
+ community feedback that they also work with Sectigo ACME Service for InCommon.
+ If you experience problems with another ACME server, please
+ L(create an issue,https://github.com/ansible-collections/community.crypto/issues/new/choose)
+ to help us supporting it. Feedback that an ACME server not mentioned does work
+ is also appreciated."
+requirements:
+ - either openssl or L(cryptography,https://cryptography.io/) >= 1.5
+ - ipaddress
+options:
+ account_key_src:
+ description:
+ - "Path to a file containing the ACME account RSA or Elliptic Curve
+ key."
+ - "Private keys can be created with the
+ M(community.crypto.openssl_privatekey) or M(community.crypto.openssl_privatekey_pipe)
+ modules. If the requisite (cryptography) is not available,
+ keys can also be created directly with the C(openssl) command line tool:
+ RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys
+ can be created with C(openssl ecparam -genkey ...). Any other tool creating
+ private keys in PEM format can be used as well."
+ - "Mutually exclusive with C(account_key_content)."
+ - "Required if C(account_key_content) is not used."
+ type: path
+ aliases: [ account_key ]
+ account_key_content:
+ description:
+ - "Content of the ACME account RSA or Elliptic Curve key."
+ - "Mutually exclusive with C(account_key_src)."
+ - "Required if C(account_key_src) is not used."
+ - "B(Warning:) the content will be written into a temporary file, which will
+ be deleted by Ansible when the module completes. Since this is an
+ important private key — it can be used to change the account key,
+ or to revoke your certificates without knowing their private keys
+ —, this might not be acceptable."
+ - "In case C(cryptography) is used, the content is not written into a
+ temporary file. It can still happen that it is written to disk by
+ Ansible in the process of moving the module with its argument to
+ the node where it is executed."
+ type: str
+ account_key_passphrase:
+ description:
+ - Phassphrase to use to decode the account key.
+ - "B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend."
+ type: str
+ version_added: 1.6.0
+ account_uri:
+ description:
+ - "If specified, assumes that the account URI is as given. If the
+ account key does not match this account, or an account with this
+ URI does not exist, the module fails."
+ type: str
+ acme_version:
+ description:
+ - "The ACME version of the endpoint."
+ - "Must be C(1) for the classic Let's Encrypt and Buypass ACME endpoints,
+ or C(2) for standardized ACME v2 endpoints."
+ - "The value C(1) is deprecated since community.crypto 2.0.0 and will be
+ removed from community.crypto 3.0.0."
+ required: true
+ type: int
+ choices: [ 1, 2 ]
+ acme_directory:
+ description:
+ - "The ACME directory to use. This is the entry point URL to access
+ the ACME CA server API."
+ - "For safety reasons the default is set to the Let's Encrypt staging
+ server (for the ACME v1 protocol). This will create technically correct,
+ but untrusted certificates."
+ - "For Let's Encrypt, all staging endpoints can be found here:
+ U(https://letsencrypt.org/docs/staging-environment/). For Buypass, all
+ endpoints can be found here:
+ U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)"
+ - "For B(Let's Encrypt), the production directory URL for ACME v2 is
+ U(https://acme-v02.api.letsencrypt.org/directory)."
+ - "For B(Buypass), the production directory URL for ACME v2 and v1 is
+ U(https://api.buypass.com/acme/directory)."
+ - "For B(ZeroSSL), the production directory URL for ACME v2 is
+ U(https://acme.zerossl.com/v2/DV90)."
+ - "For B(Sectigo), the production directory URL for ACME v2 is
+ U(https://acme-qa.secure.trust-provider.com/v2/DV)."
+ - The notes for this module contain a list of ACME services this module has
+ been tested against.
+ required: true
+ type: str
+ validate_certs:
+ description:
+ - Whether calls to the ACME directory will validate TLS certificates.
+ - "B(Warning:) Should B(only ever) be set to C(false) for testing purposes,
+ for example when testing against a local Pebble server."
+ type: bool
+ default: true
+ select_crypto_backend:
+ description:
+ - Determines which crypto backend to use.
+ - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to
+ C(openssl).
+ - If set to C(openssl), will try to use the C(openssl) binary.
+ - If set to C(cryptography), will try to use the
+ L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography, openssl ]
+ request_timeout:
+ description:
+ - The time Ansible should wait for a response from the ACME API.
+ - This timeout is applied to all HTTP(S) requests (HEAD, GET, POST).
+ type: int
+ default: 10
+ version_added: 2.3.0
+'''
diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/attributes.py b/ansible_collections/community/crypto/plugins/doc_fragments/attributes.py
new file mode 100644
index 00000000..11f6b575
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/doc_fragments/attributes.py
@@ -0,0 +1,85 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+
+ # Standard documentation fragment
+ DOCUMENTATION = r'''
+options: {}
+attributes:
+ check_mode:
+ description: Can run in C(check_mode) and return changed status prediction without modifying target.
+ diff_mode:
+ description: Will return details on what has changed (or possibly needs changing in C(check_mode)), when in diff mode.
+'''
+
+ # Should be used together with the standard fragment
+ INFO_MODULE = r'''
+options: {}
+attributes:
+ check_mode:
+ support: full
+ details:
+ - This action does not modify state.
+ diff_mode:
+ support: N/A
+ details:
+ - This action does not modify state.
+'''
+
+ ACTIONGROUP_ACME = r'''
+options: {}
+attributes:
+ action_group:
+ description: Use C(group/acme) or C(group/community.crypto.acme) in C(module_defaults) to set defaults for this module.
+ support: full
+ membership:
+ - community.crypto.acme
+ - acme
+'''
+
+ FACTS = r'''
+options: {}
+attributes:
+ facts:
+ description: Action returns an C(ansible_facts) dictionary that will update existing host facts.
+'''
+
+ # Should be used together with the standard fragment and the FACTS fragment
+ FACTS_MODULE = r'''
+options: {}
+attributes:
+ check_mode:
+ support: full
+ details:
+ - This action does not modify state.
+ diff_mode:
+ support: N/A
+ details:
+ - This action does not modify state.
+ facts:
+ support: full
+'''
+
+ FILES = r'''
+options: {}
+attributes:
+ safe_file_operations:
+ description: Uses Ansible's strict file operation functions to ensure proper permissions and avoid data corruption.
+'''
+
+ FLOW = r'''
+options: {}
+attributes:
+ action:
+ description: Indicates this has a corresponding action plugin so some parts of the options can be executed on the controller.
+ async:
+ description: Supports being used with the C(async) keyword.
+'''
diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/ecs_credential.py b/ansible_collections/community/crypto/plugins/doc_fragments/ecs_credential.py
new file mode 100644
index 00000000..0b6d4037
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/doc_fragments/ecs_credential.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c), Entrust Datacard Corporation, 2019
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+
+ # Plugin options for Entrust Certificate Services (ECS) credentials
+ DOCUMENTATION = r'''
+options:
+ entrust_api_user:
+ description:
+ - The username for authentication to the Entrust Certificate Services (ECS) API.
+ type: str
+ required: true
+ entrust_api_key:
+ description:
+ - The key (password) for authentication to the Entrust Certificate Services (ECS) API.
+ type: str
+ required: true
+ entrust_api_client_cert_path:
+ description:
+ - The path to the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
+ type: path
+ required: true
+ entrust_api_client_cert_key_path:
+ description:
+ - The path to the key for the client certificate used to authenticate to the Entrust Certificate Services (ECS) API.
+ type: path
+ required: true
+ 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.
+ type: path
+ default: https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml
+requirements:
+ - "PyYAML >= 3.11"
+'''
diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/module_certificate.py b/ansible_collections/community/crypto/plugins/doc_fragments/module_certificate.py
new file mode 100644
index 00000000..e277edfa
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/doc_fragments/module_certificate.py
@@ -0,0 +1,404 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+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 cryptography python library to interact with OpenSSL.
+requirements:
+ - cryptography >= 1.6 (if using C(selfsigned) or C(ownca) provider)
+options:
+ force:
+ description:
+ - Generate the certificate, even if it already exists.
+ type: bool
+ default: false
+
+ 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
+
+ ignore_timestamps:
+ description:
+ - Whether the "not before" and "not after" timestamps should be ignored for idempotency checks.
+ - It is better to keep the default value C(true) when using relative timestamps (like C(+0s) for now).
+ type: bool
+ default: true
+ version_added: 2.0.0
+
+ select_crypto_backend:
+ description:
+ - Determines which crypto backend to use.
+ - The default choice is C(auto), which tries to use C(cryptography) if available.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography ]
+
+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://<HOST>:80/.well-known/acme-challenge/)
+ - This is only used by the C(acme) provider.
+ type: path
+
+ acme_chain:
+ description:
+ - Include the intermediate certificate to the generated certificate
+ - This is only used by the C(acme) provider.
+ - Note that this is only available for older versions of C(acme-tiny).
+ New versions include the chain automatically, and setting I(acme_chain) to C(true) results in an error.
+ type: bool
+ default: false
+
+ 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_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.
+ - Please note that this value is B(not) covered by the I(ignore_timestamps) option.
+ 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]) (for example C(+32w1d2h)).
+ - If this value is not specified, the certificate will start being valid from now.
+ - Note that this value is B(not used to determine whether an existing certificate should be regenerated).
+ This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
+ avoid relative timestamps when setting I(ignore_timestamps=false).
+ - 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]) (for example C(+32w1d2h)).
+ - If this value is not specified, the certificate will stop being valid 10 years from now.
+ - Note that this value is B(not used to determine whether an existing certificate should be regenerated).
+ This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
+ avoid relative timestamps when setting I(ignore_timestamps=false).
+ - 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: true
+'''
+
+ 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]) (for example C(+32w1d2h)).
+ - If this value is not specified, the certificate will start being valid from now.
+ - Note that this value is B(not used to determine whether an existing certificate should be regenerated).
+ This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
+ avoid relative timestamps when setting I(ignore_timestamps=false).
+ - 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]) (for example C(+32w1d2h)).
+ - If this value is not specified, the certificate will stop being valid 10 years from now.
+ - Note that this value is B(not used to determine whether an existing certificate should be regenerated).
+ This can be changed by setting the I(ignore_timestamps) option to C(false). Please note that you should
+ avoid relative timestamps when setting I(ignore_timestamps=false).
+ - 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/ansible_collections/community/crypto/plugins/doc_fragments/module_csr.py b/ansible_collections/community/crypto/plugins/doc_fragments/module_csr.py
new file mode 100644
index 00000000..81c4318a
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/doc_fragments/module_csr.py
@@ -0,0 +1,325 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017, Yanis Guenane <yanis+ansible@guenane.org>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+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 certificate signing requests.
+ - This module supports the subjectAltName, keyUsage, extendedKeyUsage, basicConstraints and OCSP Must Staple
+ extensions.
+requirements:
+ - cryptography >= 1.3
+options:
+ digest:
+ description:
+ - The digest used when signing the certificate signing request with the private key.
+ type: str
+ default: sha256
+ privatekey_path:
+ description:
+ - The path to the private key to use when signing the certificate signing request.
+ - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
+ type: path
+ privatekey_content:
+ description:
+ - The content of the private key to use when signing the certificate signing request.
+ - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
+ type: str
+ privatekey_passphrase:
+ description:
+ - The passphrase for the private key.
+ - This is required if the private key is password protected.
+ type: str
+ version:
+ description:
+ - The version of the certificate signing request.
+ - "The only allowed value according to L(RFC 2986,https://tools.ietf.org/html/rfc2986#section-4.1)
+ is 1."
+ - This option no longer accepts unsupported values since community.crypto 2.0.0.
+ type: int
+ default: 1
+ choices:
+ - 1
+ subject:
+ description:
+ - Key/value pairs that will be present in the subject name field of the certificate signing request.
+ - If you need to specify more than one value with the same key, use a list as value.
+ - If the order of the components is important, use I(subject_ordered).
+ - Mutually exclusive with I(subject_ordered).
+ type: dict
+ subject_ordered:
+ description:
+ - A list of dictionaries, where every dictionary must contain one key/value pair. This key/value pair
+ will be present in the subject name field of the certificate signing request.
+ - If you want to specify more than one value with the same key in a row, you can use a list as value.
+ - Mutually exclusive with I(subject), and any other subject field option, such as I(country_name),
+ I(state_or_province_name), I(locality_name), I(organization_name), I(organizational_unit_name),
+ I(common_name), or I(email_address).
+ type: list
+ elements: dict
+ version_added: 2.0.0
+ country_name:
+ description:
+ - The countryName field of the certificate signing request subject.
+ type: str
+ aliases: [ C, countryName ]
+ state_or_province_name:
+ description:
+ - The stateOrProvinceName field of the certificate signing request subject.
+ type: str
+ aliases: [ ST, stateOrProvinceName ]
+ locality_name:
+ description:
+ - The localityName field of the certificate signing request subject.
+ type: str
+ aliases: [ L, localityName ]
+ organization_name:
+ description:
+ - The organizationName field of the certificate signing request subject.
+ type: str
+ aliases: [ O, organizationName ]
+ organizational_unit_name:
+ description:
+ - The organizationalUnitName field of the certificate signing request subject.
+ type: str
+ aliases: [ OU, organizationalUnitName ]
+ common_name:
+ description:
+ - The commonName field of the certificate signing request subject.
+ type: str
+ aliases: [ CN, commonName ]
+ email_address:
+ description:
+ - The emailAddress field of the certificate signing request subject.
+ type: str
+ aliases: [ E, emailAddress ]
+ subject_alt_name:
+ description:
+ - Subject Alternative Name (SAN) extension to attach to the certificate signing request.
+ - Values must be prefixed by their options. (These are C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
+ C(otherName), and the ones specific to your CA).
+ - Note that if no SAN is specified, but a common name, the common
+ name will be added as a SAN except if C(useCommonNameForSAN) is
+ set to I(false).
+ - More at U(https://tools.ietf.org/html/rfc5280#section-4.2.1.6).
+ type: list
+ elements: str
+ aliases: [ subjectAltName ]
+ subject_alt_name_critical:
+ description:
+ - Should the subjectAltName extension be considered as critical.
+ type: bool
+ default: false
+ aliases: [ subjectAltName_critical ]
+ use_common_name_for_san:
+ description:
+ - If set to C(true), the module will fill the common name in for
+ C(subject_alt_name) with C(DNS:) prefix if no SAN is specified.
+ type: bool
+ default: true
+ aliases: [ useCommonNameForSAN ]
+ key_usage:
+ description:
+ - This defines the purpose (for example encipherment, signature, certificate signing)
+ of the key contained in the certificate.
+ type: list
+ elements: str
+ aliases: [ keyUsage ]
+ key_usage_critical:
+ description:
+ - Should the keyUsage extension be considered as critical.
+ type: bool
+ default: false
+ aliases: [ keyUsage_critical ]
+ extended_key_usage:
+ description:
+ - Additional restrictions (for example client authentication, server authentication)
+ on the allowed purposes for which the public key may be used.
+ type: list
+ elements: str
+ aliases: [ extKeyUsage, extendedKeyUsage ]
+ extended_key_usage_critical:
+ description:
+ - Should the extkeyUsage extension be considered as critical.
+ type: bool
+ default: false
+ aliases: [ extKeyUsage_critical, extendedKeyUsage_critical ]
+ basic_constraints:
+ description:
+ - Indicates basic constraints, such as if the certificate is a CA.
+ type: list
+ elements: str
+ aliases: [ basicConstraints ]
+ basic_constraints_critical:
+ description:
+ - Should the basicConstraints extension be considered as critical.
+ type: bool
+ default: false
+ aliases: [ basicConstraints_critical ]
+ ocsp_must_staple:
+ description:
+ - Indicates that the certificate should contain the OCSP Must Staple
+ extension (U(https://tools.ietf.org/html/rfc7633)).
+ type: bool
+ default: false
+ aliases: [ ocspMustStaple ]
+ ocsp_must_staple_critical:
+ description:
+ - Should the OCSP Must Staple extension be considered as critical.
+ - Note that according to the RFC, this extension should not be marked
+ as critical, as old clients not knowing about OCSP Must Staple
+ are required to reject such certificates
+ (see U(https://tools.ietf.org/html/rfc7633#section-4)).
+ type: bool
+ default: false
+ aliases: [ ocspMustStaple_critical ]
+ name_constraints_permitted:
+ description:
+ - For CA certificates, this specifies a list of identifiers which describe
+ subtrees of names that this CA is allowed to issue certificates for.
+ - Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
+ C(otherName) and the ones specific to your CA).
+ type: list
+ elements: str
+ name_constraints_excluded:
+ description:
+ - For CA certificates, this specifies a list of identifiers which describe
+ subtrees of names that this CA is B(not) allowed to issue certificates for.
+ - Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
+ C(otherName) and the ones specific to your CA).
+ type: list
+ elements: str
+ name_constraints_critical:
+ description:
+ - Should the Name Constraints extension be considered as critical.
+ type: bool
+ default: false
+ select_crypto_backend:
+ description:
+ - Determines which crypto backend to use.
+ - The default choice is C(auto), which tries to use C(cryptography) if available.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography ]
+ create_subject_key_identifier:
+ description:
+ - Create the Subject Key Identifier from the public key.
+ - "Please note that commercial CAs can ignore the value, respectively use a value of
+ their own choice instead. Specifying this option is mostly useful for self-signed
+ certificates or for own CAs."
+ - Note that this is only supported if the C(cryptography) backend is used!
+ type: bool
+ default: false
+ subject_key_identifier:
+ description:
+ - The subject key identifier as a hex string, where two bytes are separated by colons.
+ - "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)"
+ - "Please note that commercial CAs ignore this value, respectively use a value of their
+ own choice. Specifying this option is mostly useful for self-signed certificates
+ or for own CAs."
+ - Note that this option can only be used if I(create_subject_key_identifier) is C(false).
+ - Note that this is only supported if the C(cryptography) backend is used!
+ type: str
+ authority_key_identifier:
+ description:
+ - The authority key identifier as a hex string, where two bytes are separated by colons.
+ - "Example: C(00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33)"
+ - "Please note that commercial CAs ignore this value, respectively use a value of their
+ own choice. Specifying this option is mostly useful for self-signed certificates
+ or for own CAs."
+ - Note that this is only supported if the C(cryptography) backend is used!
+ - The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
+ I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
+ type: str
+ authority_cert_issuer:
+ description:
+ - Names that will be present in the authority cert issuer field of the certificate signing request.
+ - Values must be prefixed by their options. (i.e., C(email), C(URI), C(DNS), C(RID), C(IP), C(dirName),
+ C(otherName) and the ones specific to your CA)
+ - "Example: C(DNS:ca.example.org)"
+ - If specified, I(authority_cert_serial_number) must also be specified.
+ - "Please note that commercial CAs ignore this value, respectively use a value of their
+ own choice. Specifying this option is mostly useful for self-signed certificates
+ or for own CAs."
+ - Note that this is only supported if the C(cryptography) backend is used!
+ - The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
+ I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
+ type: list
+ elements: str
+ authority_cert_serial_number:
+ description:
+ - The authority cert serial number.
+ - If specified, I(authority_cert_issuer) must also be specified.
+ - Note that this is only supported if the C(cryptography) backend is used!
+ - "Please note that commercial CAs ignore this value, respectively use a value of their
+ own choice. Specifying this option is mostly useful for self-signed certificates
+ or for own CAs."
+ - The C(AuthorityKeyIdentifier) extension will only be added if at least one of I(authority_key_identifier),
+ I(authority_cert_issuer) and I(authority_cert_serial_number) is specified.
+ type: int
+ crl_distribution_points:
+ description:
+ - Allows to specify one or multiple CRL distribution points.
+ - Only supported by the C(cryptography) backend.
+ type: list
+ elements: dict
+ suboptions:
+ full_name:
+ description:
+ - Describes how the CRL can be retrieved.
+ - Mutually exclusive with I(relative_name).
+ - "Example: C(URI:https://ca.example.com/revocations.crl)."
+ type: list
+ elements: str
+ relative_name:
+ description:
+ - Describes how the CRL can be retrieved relative to the CRL issuer.
+ - Mutually exclusive with I(full_name).
+ - "Example: C(/CN=example.com)."
+ - Can only be used when cryptography >= 1.6 is installed.
+ type: list
+ elements: str
+ crl_issuer:
+ description:
+ - Information about the issuer of the CRL.
+ type: list
+ elements: str
+ reasons:
+ description:
+ - List of reasons that this distribution point can be used for when performing revocation checks.
+ type: list
+ elements: str
+ choices:
+ - key_compromise
+ - ca_compromise
+ - affiliation_changed
+ - superseded
+ - cessation_of_operation
+ - certificate_hold
+ - privilege_withdrawn
+ - aa_compromise
+ version_added: 1.4.0
+notes:
+ - If the certificate signing request already exists it will be checked whether subjectAltName,
+ keyUsage, extendedKeyUsage and basicConstraints only contain the requested values, whether
+ OCSP Must Staple is as requested, and if the request was signed by the given private key.
+seealso:
+- module: 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/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey.py b/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey.py
new file mode 100644
index 00000000..a27b26c7
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey.py
@@ -0,0 +1,151 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+
+ # Standard files documentation fragment
+ DOCUMENTATION = r'''
+description:
+ - One can generate L(RSA,https://en.wikipedia.org/wiki/RSA_%28cryptosystem%29),
+ L(DSA,https://en.wikipedia.org/wiki/Digital_Signature_Algorithm),
+ L(ECC,https://en.wikipedia.org/wiki/Elliptic-curve_cryptography) or
+ L(EdDSA,https://en.wikipedia.org/wiki/EdDSA) private keys.
+ - Keys are generated in PEM format.
+ - "Please note that the module regenerates private keys if they do not match
+ the module's options. In particular, if you provide another passphrase
+ (or specify none), change the keysize, etc., the private key will be
+ regenerated. If you are concerned that this could B(overwrite your private key),
+ consider using the I(backup) option."
+requirements:
+ - cryptography >= 1.2.3 (older versions might work as well)
+options:
+ size:
+ description:
+ - Size (in bits) of the TLS/SSL key to generate.
+ type: int
+ default: 4096
+ type:
+ description:
+ - The algorithm used to generate the TLS/SSL private key.
+ - Note that C(ECC), C(X25519), C(X448), C(Ed25519) and C(Ed448) require the C(cryptography) backend.
+ C(X25519) needs cryptography 2.5 or newer, while C(X448), C(Ed25519) and C(Ed448) require
+ cryptography 2.6 or newer. For C(ECC), the minimal cryptography version required depends on the
+ I(curve) option.
+ type: str
+ default: RSA
+ choices: [ DSA, ECC, Ed25519, Ed448, RSA, X25519, X448 ]
+ curve:
+ description:
+ - Note that not all curves are supported by all versions of C(cryptography).
+ - For maximal interoperability, C(secp384r1) or C(secp256r1) should be used.
+ - We use the curve names as defined in the
+ L(IANA registry for TLS,https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-8).
+ - Please note that all curves except C(secp224r1), C(secp256k1), C(secp256r1), C(secp384r1) and C(secp521r1)
+ are discouraged for new private keys.
+ type: str
+ choices:
+ - secp224r1
+ - secp256k1
+ - secp256r1
+ - secp384r1
+ - secp521r1
+ - secp192r1
+ - brainpoolP256r1
+ - brainpoolP384r1
+ - brainpoolP512r1
+ - sect163k1
+ - sect163r2
+ - sect233k1
+ - sect233r1
+ - sect283k1
+ - sect283r1
+ - sect409k1
+ - sect409r1
+ - sect571k1
+ - sect571r1
+ passphrase:
+ description:
+ - The passphrase for the private key.
+ type: str
+ cipher:
+ description:
+ - The cipher to encrypt the private key. Must be C(auto).
+ 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.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography ]
+ format:
+ description:
+ - Determines which format the private key is written in. By default, PKCS1 (traditional OpenSSL format)
+ is used for all keys which support it. Please note that not every key can be exported in any format.
+ - The value C(auto) selects a format based on the key format. The value C(auto_ignore) does the same,
+ but for existing private key files, it will not force a regenerate when its format is not the automatically
+ selected one for generation.
+ - Note that if the format for an existing private key mismatches, the key is B(regenerated) by default.
+ To change this behavior, use the I(format_mismatch) option.
+ type: str
+ default: auto_ignore
+ choices: [ pkcs1, pkcs8, raw, auto, auto_ignore ]
+ format_mismatch:
+ description:
+ - Determines behavior of the module if the format of a private key does not match the expected format, but all
+ other parameters are as expected.
+ - If set to C(regenerate) (default), generates a new private key.
+ - If set to C(convert), the key will be converted to the new format instead.
+ - Only supported by the C(cryptography) backend.
+ type: str
+ default: regenerate
+ choices: [ regenerate, convert ]
+ regenerate:
+ description:
+ - Allows to configure in which situations the module is allowed to regenerate private keys.
+ The module will always generate a new key if the destination file does not exist.
+ - By default, the key will be regenerated when it does not match the module's options,
+ except when the key cannot be read or the passphrase does not match. Please note that
+ this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence)
+ is specified.
+ - If set to C(never), the module will fail if the key cannot be read or the passphrase
+ is not matching, and will never regenerate an existing key.
+ - If set to C(fail), the module will fail if the key does not correspond to the module's
+ options.
+ - If set to C(partial_idempotence), the key will be regenerated if it does not conform to
+ the module's options. The key is B(not) regenerated if it cannot be read (broken file),
+ the key is protected by an unknown passphrase, or when they key is not protected by a
+ passphrase, but a passphrase is specified.
+ - If set to C(full_idempotence), the key will be regenerated if it does not conform to the
+ module's options. This is also the case if the key cannot be read (broken file), the key
+ is protected by an unknown passphrase, or when they key is not protected by a passphrase,
+ but a passphrase is specified. Make sure you have a B(backup) when using this option!
+ - If set to C(always), the module will always regenerate the key. This is equivalent to
+ setting I(force) to C(true).
+ - Note that if I(format_mismatch) is set to C(convert) and everything matches except the
+ format, the key will always be converted, except if I(regenerate) is set to C(always).
+ type: str
+ choices:
+ - never
+ - fail
+ - partial_idempotence
+ - full_idempotence
+ - always
+ default: full_idempotence
+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/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey_convert.py b/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey_convert.py
new file mode 100644
index 00000000..f1c6f70e
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/doc_fragments/module_privatekey_convert.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+
+ # Standard files documentation fragment
+ DOCUMENTATION = r'''
+requirements:
+ - cryptography >= 1.2.3 (older versions might work as well)
+options:
+ src_path:
+ description:
+ - Name of the file containing the OpenSSL private key to convert.
+ - Exactly one of I(src_path) or I(src_content) must be specified.
+ type: path
+ src_content:
+ description:
+ - The content of the file containing the OpenSSL private key to convert.
+ - Exactly one of I(src_path) or I(src_content) must be specified.
+ type: str
+ src_passphrase:
+ description:
+ - The passphrase for the private key to load.
+ type: str
+ dest_passphrase:
+ description:
+ - The passphrase for the private key to store.
+ type: str
+ format:
+ description:
+ - Determines which format the destination private key should be written in.
+ - Please note that not every key can be exported in any format, and that not every
+ format supports encryption.
+ type: str
+ choices: [ pkcs1, pkcs8, raw ]
+ required: true
+seealso:
+ - module: community.crypto.openssl_privatekey
+ - module: community.crypto.openssl_privatekey_pipe
+ - module: community.crypto.openssl_publickey
+'''
diff --git a/ansible_collections/community/crypto/plugins/doc_fragments/name_encoding.py b/ansible_collections/community/crypto/plugins/doc_fragments/name_encoding.py
new file mode 100644
index 00000000..fec94380
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/doc_fragments/name_encoding.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+class ModuleDocFragment(object):
+ DOCUMENTATION = r'''
+options:
+ name_encoding:
+ description:
+ - How to encode names (DNS names, URIs, email addresses) in return values.
+ - C(ignore) will use the encoding returned by the backend.
+ - C(idna) will convert all labels of domain names to IDNA encoding.
+ IDNA2008 will be preferred, and IDNA2003 will be used if IDNA2008 encoding fails.
+ - C(unicode) will convert all labels of domain names to Unicode.
+ IDNA2008 will be preferred, and IDNA2003 will be used if IDNA2008 decoding fails.
+ - B(Note) that C(idna) and C(unicode) require the L(idna Python library,https://pypi.org/project/idna/) to be installed.
+ type: str
+ default: ignore
+ choices:
+ - ignore
+ - idna
+ - unicode
+requirements:
+ - If I(name_encoding) is set to another value than C(ignore), the L(idna Python library,https://pypi.org/project/idna/) needs to be installed.
+'''
diff --git a/ansible_collections/community/crypto/plugins/filter/openssl_csr_info.py b/ansible_collections/community/crypto/plugins/filter/openssl_csr_info.py
new file mode 100644
index 00000000..851dfe2a
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/filter/openssl_csr_info.py
@@ -0,0 +1,313 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+name: openssl_csr_info
+short_description: Retrieve information from OpenSSL Certificate Signing Requests (CSR)
+version_added: 2.10.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Provided an OpenSSL Certificate Signing Requests (CSR), retrieve information.
+ - This is a filter version of the M(community.crypto.openssl_csr_info) module.
+options:
+ _input:
+ description:
+ - The content of the OpenSSL CSR.
+ type: string
+ required: true
+extends_documentation_fragment:
+ - community.crypto.name_encoding
+seealso:
+ - module: community.crypto.openssl_csr_info
+'''
+
+EXAMPLES = '''
+- name: Show the Subject Alt Names of the CSR
+ ansible.builtin.debug:
+ msg: >-
+ {{
+ (
+ lookup('ansible.builtin.file', '/path/to/cert.csr')
+ | community.crypto.openssl_csr_info
+ ).subject_alt_name | join(', ')
+ }}
+'''
+
+RETURN = '''
+_value:
+ description:
+ - Information on the certificate.
+ type: dict
+ contains:
+ signature_valid:
+ description:
+ - Whether the CSR's signature is valid.
+ - In case the check returns C(false), the module will fail.
+ returned: success
+ type: bool
+ basic_constraints:
+ description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present.
+ returned: success
+ type: list
+ elements: str
+ sample: ['CA:TRUE', 'pathlen:1']
+ basic_constraints_critical:
+ description: Whether the C(basic_constraints) extension is critical.
+ returned: success
+ type: bool
+ extended_key_usage:
+ description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present.
+ returned: success
+ type: list
+ elements: str
+ sample: [Biometric Info, DVCS, Time Stamping]
+ extended_key_usage_critical:
+ description: Whether the C(extended_key_usage) extension is critical.
+ returned: success
+ type: bool
+ extensions_by_oid:
+ description: Returns a dictionary for every extension OID
+ returned: success
+ type: dict
+ contains:
+ critical:
+ description: Whether the extension is critical.
+ returned: success
+ type: bool
+ value:
+ description:
+ - The Base64 encoded value (in DER format) of the extension.
+ - B(Note) that depending on the C(cryptography) version used, it is
+ not possible to extract the ASN.1 content of the extension, but only
+ to provide the re-encoded content of the extension in case it was
+ parsed by C(cryptography). This should usually result in exactly the
+ same value, except if the original extension value was malformed.
+ returned: success
+ type: str
+ sample: "MAMCAQU="
+ sample: {"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}
+ key_usage:
+ description: Entries in the C(key_usage) extension, or C(none) if extension is not present.
+ returned: success
+ type: str
+ sample: [Key Agreement, Data Encipherment]
+ key_usage_critical:
+ description: Whether the C(key_usage) extension is critical.
+ returned: success
+ type: bool
+ subject_alt_name:
+ description:
+ - Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: list
+ elements: str
+ sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
+ subject_alt_name_critical:
+ description: Whether the C(subject_alt_name) extension is critical.
+ returned: success
+ type: bool
+ ocsp_must_staple:
+ description: C(true) if the OCSP Must Staple extension is present, C(none) otherwise.
+ returned: success
+ type: bool
+ ocsp_must_staple_critical:
+ description: Whether the C(ocsp_must_staple) extension is critical.
+ returned: success
+ type: bool
+ name_constraints_permitted:
+ description: List of permitted subtrees to sign certificates for.
+ returned: success
+ type: list
+ elements: str
+ sample: ['email:.somedomain.com']
+ name_constraints_excluded:
+ description:
+ - List of excluded subtrees the CA cannot sign certificates for.
+ - Is C(none) if extension is not present.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: list
+ elements: str
+ sample: ['email:.com']
+ name_constraints_critical:
+ description:
+ - Whether the C(name_constraints) extension is critical.
+ - Is C(none) if extension is not present.
+ returned: success
+ type: bool
+ subject:
+ description:
+ - The CSR's subject as a dictionary.
+ - Note that for repeated values, only the last one will be returned.
+ returned: success
+ type: dict
+ sample: {"commonName": "www.example.com", "emailAddress": "test@example.com"}
+ subject_ordered:
+ description: The CSR's subject as an ordered list of tuples.
+ returned: success
+ type: list
+ elements: list
+ sample: [["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]
+ public_key:
+ description: CSR's public key in PEM format
+ returned: success
+ type: str
+ sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
+ public_key_type:
+ description:
+ - The CSR's public key's type.
+ - One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
+ - Will start with C(unknown) if the key type cannot be determined.
+ returned: success
+ type: str
+ sample: RSA
+ public_key_data:
+ description:
+ - Public key data. Depends on the public key's type.
+ returned: success
+ type: dict
+ contains:
+ size:
+ description:
+ - Bit size of modulus (RSA) or prime number (DSA).
+ type: int
+ returned: When C(public_key_type=RSA) or C(public_key_type=DSA)
+ modulus:
+ description:
+ - The RSA key's modulus.
+ type: int
+ returned: When C(public_key_type=RSA)
+ exponent:
+ description:
+ - The RSA key's public exponent.
+ type: int
+ returned: When C(public_key_type=RSA)
+ p:
+ description:
+ - The C(p) value for DSA.
+ - This is the prime modulus upon which arithmetic takes place.
+ type: int
+ returned: When C(public_key_type=DSA)
+ q:
+ description:
+ - The C(q) value for DSA.
+ - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
+ multiplicative group of the prime field used.
+ type: int
+ returned: When C(public_key_type=DSA)
+ g:
+ description:
+ - The C(g) value for DSA.
+ - This is the element spanning the subgroup of the multiplicative group of the prime field used.
+ type: int
+ returned: When C(public_key_type=DSA)
+ curve:
+ description:
+ - The curve's name for ECC.
+ type: str
+ returned: When C(public_key_type=ECC)
+ exponent_size:
+ description:
+ - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
+ type: int
+ returned: When C(public_key_type=ECC)
+ x:
+ description:
+ - The C(x) coordinate for the public point on the elliptic curve.
+ type: int
+ returned: When C(public_key_type=ECC)
+ y:
+ description:
+ - For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
+ - For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
+ type: int
+ returned: When C(public_key_type=DSA) or C(public_key_type=ECC)
+ public_key_fingerprints:
+ description:
+ - Fingerprints of CSR's public key.
+ - For every hash algorithm available, the fingerprint is computed.
+ returned: success
+ type: dict
+ sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
+ 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
+ subject_key_identifier:
+ description:
+ - The CSR's subject key identifier.
+ - The identifier is returned in hexadecimal, with C(:) used to separate bytes.
+ - Is C(none) if the C(SubjectKeyIdentifier) extension is not present.
+ returned: success
+ type: str
+ sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
+ authority_key_identifier:
+ description:
+ - The CSR's authority key identifier.
+ - The identifier is returned in hexadecimal, with C(:) used to separate bytes.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ returned: success
+ type: str
+ sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
+ authority_cert_issuer:
+ description:
+ - The CSR's authority cert issuer as a list of general names.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: list
+ elements: str
+ sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
+ authority_cert_serial_number:
+ description:
+ - The CSR's authority cert serial number.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ returned: success
+ type: int
+ sample: 12345
+'''
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils.six import string_types
+from ansible.module_utils.common.text.converters import to_bytes, to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import (
+ get_csr_info,
+)
+
+from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock
+
+
+def openssl_csr_info_filter(data, name_encoding='ignore'):
+ '''Extract information from X.509 PEM certificate.'''
+ if not isinstance(data, string_types):
+ raise AnsibleFilterError('The community.crypto.openssl_csr_info input must be a text type, not %s' % type(data))
+ if not isinstance(name_encoding, string_types):
+ raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding))
+ name_encoding = to_native(name_encoding)
+ if name_encoding not in ('ignore', 'idna', 'unicode'):
+ raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding)
+
+ module = FilterModuleMock({'name_encoding': name_encoding})
+ try:
+ return get_csr_info(module, 'cryptography', content=to_bytes(data), validate_signature=True)
+ except OpenSSLObjectError as exc:
+ raise AnsibleFilterError(to_native(exc))
+
+
+class FilterModule(object):
+ '''Ansible jinja2 filters'''
+
+ def filters(self):
+ return {
+ 'openssl_csr_info': openssl_csr_info_filter,
+ }
diff --git a/ansible_collections/community/crypto/plugins/filter/openssl_privatekey_info.py b/ansible_collections/community/crypto/plugins/filter/openssl_privatekey_info.py
new file mode 100644
index 00000000..16dfd859
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/filter/openssl_privatekey_info.py
@@ -0,0 +1,193 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+name: openssl_privatekey_info
+short_description: Retrieve information from OpenSSL private keys
+version_added: 2.10.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Provided an OpenSSL private keys, retrieve information.
+ - This is a filter version of the M(community.crypto.openssl_privatekey_info) module.
+options:
+ _input:
+ description:
+ - The content of the OpenSSL private key.
+ type: string
+ required: true
+ passphrase:
+ description:
+ - The passphrase for the private key.
+ type: str
+ return_private_key_data:
+ description:
+ - Whether to return private key data.
+ - Only set this to C(true) when you want private information about this key to
+ be extracted.
+ - "B(WARNING:) you have to make sure that private key data is not accidentally logged!"
+ type: bool
+ default: false
+extends_documentation_fragment:
+ - community.crypto.name_encoding
+seealso:
+ - module: community.crypto.openssl_privatekey_info
+'''
+
+EXAMPLES = '''
+- name: Show the Subject Alt Names of the CSR
+ ansible.builtin.debug:
+ msg: >-
+ {{
+ (
+ lookup('ansible.builtin.file', '/path/to/cert.csr')
+ | community.crypto.openssl_privatekey_info
+ ).subject_alt_name | join(', ')
+ }}
+'''
+
+RETURN = '''
+_value:
+ description:
+ - Information on the certificate.
+ type: dict
+ contains:
+ public_key:
+ description: Private key's public key in PEM format.
+ returned: success
+ type: str
+ sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
+ public_key_fingerprints:
+ description:
+ - Fingerprints of private key's public key.
+ - For every hash algorithm available, the fingerprint is computed.
+ returned: success
+ type: dict
+ sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
+ 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
+ type:
+ description:
+ - The key's type.
+ - One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
+ - Will start with C(unknown) if the key type cannot be determined.
+ returned: success
+ type: str
+ sample: RSA
+ public_data:
+ description:
+ - Public key data. Depends on key type.
+ returned: success
+ type: dict
+ contains:
+ size:
+ description:
+ - Bit size of modulus (RSA) or prime number (DSA).
+ type: int
+ returned: When C(type=RSA) or C(type=DSA)
+ modulus:
+ description:
+ - The RSA key's modulus.
+ type: int
+ returned: When C(type=RSA)
+ exponent:
+ description:
+ - The RSA key's public exponent.
+ type: int
+ returned: When C(type=RSA)
+ p:
+ description:
+ - The C(p) value for DSA.
+ - This is the prime modulus upon which arithmetic takes place.
+ type: int
+ returned: When C(type=DSA)
+ q:
+ description:
+ - The C(q) value for DSA.
+ - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
+ multiplicative group of the prime field used.
+ type: int
+ returned: When C(type=DSA)
+ g:
+ description:
+ - The C(g) value for DSA.
+ - This is the element spanning the subgroup of the multiplicative group of the prime field used.
+ type: int
+ returned: When C(type=DSA)
+ curve:
+ description:
+ - The curve's name for ECC.
+ type: str
+ returned: When C(type=ECC)
+ exponent_size:
+ description:
+ - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
+ type: int
+ returned: When C(type=ECC)
+ x:
+ description:
+ - The C(x) coordinate for the public point on the elliptic curve.
+ type: int
+ returned: When C(type=ECC)
+ y:
+ description:
+ - For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
+ - For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
+ type: int
+ returned: When C(type=DSA) or C(type=ECC)
+ private_data:
+ description:
+ - Private key data. Depends on key type.
+ returned: success and when I(return_private_key_data) is set to C(true)
+ type: dict
+'''
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils.six import string_types
+from ansible.module_utils.common.text.converters import to_bytes, to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import (
+ PrivateKeyParseError,
+ get_privatekey_info,
+)
+
+from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock
+
+
+def openssl_privatekey_info_filter(data, passphrase=None, return_private_key_data=False):
+ '''Extract information from X.509 PEM certificate.'''
+ if not isinstance(data, string_types):
+ raise AnsibleFilterError('The community.crypto.openssl_privatekey_info input must be a text type, not %s' % type(data))
+ if passphrase is not None and not isinstance(passphrase, string_types):
+ raise AnsibleFilterError('The passphrase option must be a text type, not %s' % type(passphrase))
+ if not isinstance(return_private_key_data, bool):
+ raise AnsibleFilterError('The return_private_key_data option must be a boolean, not %s' % type(return_private_key_data))
+
+ module = FilterModuleMock({})
+ try:
+ result = get_privatekey_info(module, 'cryptography', content=to_bytes(data), passphrase=passphrase, return_private_key_data=return_private_key_data)
+ result.pop('can_parse_key', None)
+ result.pop('key_is_consistent', None)
+ return result
+ except PrivateKeyParseError as exc:
+ raise AnsibleFilterError(exc.error_message)
+ except OpenSSLObjectError as exc:
+ raise AnsibleFilterError(to_native(exc))
+
+
+class FilterModule(object):
+ '''Ansible jinja2 filters'''
+
+ def filters(self):
+ return {
+ 'openssl_privatekey_info': openssl_privatekey_info_filter,
+ }
diff --git a/ansible_collections/community/crypto/plugins/filter/openssl_publickey_info.py b/ansible_collections/community/crypto/plugins/filter/openssl_publickey_info.py
new file mode 100644
index 00000000..f41af1c7
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/filter/openssl_publickey_info.py
@@ -0,0 +1,162 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+name: openssl_publickey_info
+short_description: Retrieve information from OpenSSL public keys in PEM format
+version_added: 2.10.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Provided a public key in OpenSSL PEM format, retrieve information.
+ - This is a filter version of the M(community.crypto.openssl_publickey_info) module.
+options:
+ _input:
+ description:
+ - The content of the OpenSSL PEM public key.
+ type: string
+ required: true
+seealso:
+ - module: community.crypto.openssl_publickey_info
+'''
+
+EXAMPLES = '''
+- name: Show the type of a public key
+ ansible.builtin.debug:
+ msg: >-
+ {{
+ (
+ lookup('ansible.builtin.file', '/path/to/public-key.pem')
+ | community.crypto.openssl_publickey_info
+ ).type
+ }}
+'''
+
+RETURN = '''
+_value:
+ description:
+ - Information on the public key.
+ type: dict
+ contains:
+ fingerprints:
+ description:
+ - Fingerprints of public key.
+ - For every hash algorithm available, the fingerprint is computed.
+ returned: success
+ type: dict
+ sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
+ 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
+ type:
+ description:
+ - The key's type.
+ - One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
+ - Will start with C(unknown) if the key type cannot be determined.
+ returned: success
+ type: str
+ sample: RSA
+ public_data:
+ description:
+ - Public key data. Depends on key type.
+ returned: success
+ type: dict
+ contains:
+ size:
+ description:
+ - Bit size of modulus (RSA) or prime number (DSA).
+ type: int
+ returned: When C(type=RSA) or C(type=DSA)
+ modulus:
+ description:
+ - The RSA key's modulus.
+ type: int
+ returned: When C(type=RSA)
+ exponent:
+ description:
+ - The RSA key's public exponent.
+ type: int
+ returned: When C(type=RSA)
+ p:
+ description:
+ - The C(p) value for DSA.
+ - This is the prime modulus upon which arithmetic takes place.
+ type: int
+ returned: When C(type=DSA)
+ q:
+ description:
+ - The C(q) value for DSA.
+ - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
+ multiplicative group of the prime field used.
+ type: int
+ returned: When C(type=DSA)
+ g:
+ description:
+ - The C(g) value for DSA.
+ - This is the element spanning the subgroup of the multiplicative group of the prime field used.
+ type: int
+ returned: When C(type=DSA)
+ curve:
+ description:
+ - The curve's name for ECC.
+ type: str
+ returned: When C(type=ECC)
+ exponent_size:
+ description:
+ - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
+ type: int
+ returned: When C(type=ECC)
+ x:
+ description:
+ - The C(x) coordinate for the public point on the elliptic curve.
+ type: int
+ returned: When C(type=ECC)
+ y:
+ description:
+ - For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
+ - For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
+ type: int
+ returned: When C(type=DSA) or C(type=ECC)
+'''
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils.six import string_types
+from ansible.module_utils.common.text.converters import to_bytes, to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
+ PublicKeyParseError,
+ get_publickey_info,
+)
+
+from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock
+
+
+def openssl_publickey_info_filter(data):
+ '''Extract information from OpenSSL PEM public key.'''
+ if not isinstance(data, string_types):
+ raise AnsibleFilterError('The community.crypto.openssl_publickey_info input must be a text type, not %s' % type(data))
+
+ module = FilterModuleMock({})
+ try:
+ return get_publickey_info(module, 'cryptography', content=to_bytes(data))
+ except PublicKeyParseError as exc:
+ raise AnsibleFilterError(exc.error_message)
+ except OpenSSLObjectError as exc:
+ raise AnsibleFilterError(to_native(exc))
+
+
+class FilterModule(object):
+ '''Ansible jinja2 filters'''
+
+ def filters(self):
+ return {
+ 'openssl_publickey_info': openssl_publickey_info_filter,
+ }
diff --git a/ansible_collections/community/crypto/plugins/filter/split_pem.py b/ansible_collections/community/crypto/plugins/filter/split_pem.py
new file mode 100644
index 00000000..a58ce506
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/filter/split_pem.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+name: split_pem
+short_description: Split PEM file contents into multiple objects
+version_added: 2.10.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Split PEM file contents into multiple PEM objects. Comments or invalid parts are ignored.
+options:
+ _input:
+ description:
+ - The PEM contents to split.
+ type: string
+ required: true
+'''
+
+EXAMPLES = '''
+- name: Print all CA certificates
+ ansible.builtin.debug:
+ msg: '{{ item }}'
+ loop: >-
+ {{ lookup('ansible.builtin.file', '/path/to/ca-bundle.pem') | community.crypto.split_pem }}
+'''
+
+RETURN = '''
+_value:
+ description:
+ - A list of PEM file contents.
+ type: list
+ elements: string
+'''
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils.six import string_types
+from ansible.module_utils.common.text.converters import to_text
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import split_pem_list
+
+
+def split_pem_filter(data):
+ '''Split PEM file.'''
+ if not isinstance(data, string_types):
+ raise AnsibleFilterError('The community.crypto.split_pem input must be a text type, not %s' % type(data))
+
+ data = to_text(data)
+ return split_pem_list(data)
+
+
+class FilterModule(object):
+ '''Ansible jinja2 filters'''
+
+ def filters(self):
+ return {
+ 'split_pem': split_pem_filter,
+ }
diff --git a/ansible_collections/community/crypto/plugins/filter/x509_certificate_info.py b/ansible_collections/community/crypto/plugins/filter/x509_certificate_info.py
new file mode 100644
index 00000000..21aee98a
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/filter/x509_certificate_info.py
@@ -0,0 +1,346 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+name: x509_certificate_info
+short_description: Retrieve information from X.509 certificates in PEM format
+version_added: 2.10.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Provided a X.509 certificate in PEM format, retrieve information.
+ - This is a filter version of the M(community.crypto.x509_certificate_info) module.
+options:
+ _input:
+ description:
+ - The content of the X.509 certificate in PEM format.
+ type: string
+ required: true
+extends_documentation_fragment:
+ - community.crypto.name_encoding
+seealso:
+ - module: community.crypto.x509_certificate_info
+'''
+
+EXAMPLES = '''
+- name: Show the Subject Alt Names of the certificate
+ ansible.builtin.debug:
+ msg: >-
+ {{
+ (
+ lookup('ansible.builtin.file', '/path/to/cert.pem')
+ | community.crypto.x509_certificate_info
+ ).subject_alt_name | join(', ')
+ }}
+'''
+
+RETURN = '''
+_value:
+ description:
+ - Information on the certificate.
+ type: dict
+ contains:
+ expired:
+ description: Whether the certificate is expired (in other words, C(notAfter) is in the past).
+ returned: success
+ type: bool
+ basic_constraints:
+ description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present.
+ returned: success
+ type: list
+ elements: str
+ sample: ["CA:TRUE", "pathlen:1"]
+ basic_constraints_critical:
+ description: Whether the C(basic_constraints) extension is critical.
+ returned: success
+ type: bool
+ extended_key_usage:
+ description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present.
+ returned: success
+ type: list
+ elements: str
+ sample: [Biometric Info, DVCS, Time Stamping]
+ extended_key_usage_critical:
+ description: Whether the C(extended_key_usage) extension is critical.
+ returned: success
+ type: bool
+ extensions_by_oid:
+ description: Returns a dictionary for every extension OID.
+ returned: success
+ type: dict
+ contains:
+ critical:
+ description: Whether the extension is critical.
+ returned: success
+ type: bool
+ value:
+ description:
+ - The Base64 encoded value (in DER format) of the extension.
+ - B(Note) that depending on the C(cryptography) version used, it is
+ not possible to extract the ASN.1 content of the extension, but only
+ to provide the re-encoded content of the extension in case it was
+ parsed by C(cryptography). This should usually result in exactly the
+ same value, except if the original extension value was malformed.
+ returned: success
+ type: str
+ sample: "MAMCAQU="
+ sample: {"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}
+ key_usage:
+ description: Entries in the C(key_usage) extension, or C(none) if extension is not present.
+ returned: success
+ type: str
+ sample: [Key Agreement, Data Encipherment]
+ key_usage_critical:
+ description: Whether the C(key_usage) extension is critical.
+ returned: success
+ type: bool
+ subject_alt_name:
+ description:
+ - Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: list
+ elements: str
+ sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
+ subject_alt_name_critical:
+ description: Whether the C(subject_alt_name) extension is critical.
+ returned: success
+ type: bool
+ ocsp_must_staple:
+ description: C(true) if the OCSP Must Staple extension is present, C(none) otherwise.
+ returned: success
+ type: bool
+ ocsp_must_staple_critical:
+ description: Whether the C(ocsp_must_staple) extension is critical.
+ returned: success
+ type: bool
+ issuer:
+ description:
+ - The certificate's issuer.
+ - Note that for repeated values, only the last one will be returned.
+ returned: success
+ type: dict
+ sample: {"organizationName": "Ansible", "commonName": "ca.example.com"}
+ issuer_ordered:
+ description: The certificate's issuer as an ordered list of tuples.
+ returned: success
+ type: list
+ elements: list
+ sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]]
+ subject:
+ description:
+ - The certificate's subject as a dictionary.
+ - Note that for repeated values, only the last one will be returned.
+ returned: success
+ type: dict
+ sample: {"commonName": "www.example.com", "emailAddress": "test@example.com"}
+ subject_ordered:
+ description: The certificate's subject as an ordered list of tuples.
+ returned: success
+ type: list
+ elements: list
+ sample: [["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]
+ not_after:
+ description: C(notAfter) date as ASN.1 TIME.
+ returned: success
+ type: str
+ sample: '20190413202428Z'
+ not_before:
+ description: C(notBefore) date as ASN.1 TIME.
+ returned: success
+ type: str
+ sample: '20190331202428Z'
+ public_key:
+ description: Certificate's public key in PEM format.
+ returned: success
+ type: str
+ sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
+ public_key_type:
+ description:
+ - The certificate's public key's type.
+ - One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
+ - Will start with C(unknown) if the key type cannot be determined.
+ returned: success
+ type: str
+ sample: RSA
+ public_key_data:
+ description:
+ - Public key data. Depends on the public key's type.
+ returned: success
+ type: dict
+ contains:
+ size:
+ description:
+ - Bit size of modulus (RSA) or prime number (DSA).
+ type: int
+ returned: When C(public_key_type=RSA) or C(public_key_type=DSA)
+ modulus:
+ description:
+ - The RSA key's modulus.
+ type: int
+ returned: When C(public_key_type=RSA)
+ exponent:
+ description:
+ - The RSA key's public exponent.
+ type: int
+ returned: When C(public_key_type=RSA)
+ p:
+ description:
+ - The C(p) value for DSA.
+ - This is the prime modulus upon which arithmetic takes place.
+ type: int
+ returned: When C(public_key_type=DSA)
+ q:
+ description:
+ - The C(q) value for DSA.
+ - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
+ multiplicative group of the prime field used.
+ type: int
+ returned: When C(public_key_type=DSA)
+ g:
+ description:
+ - The C(g) value for DSA.
+ - This is the element spanning the subgroup of the multiplicative group of the prime field used.
+ type: int
+ returned: When C(public_key_type=DSA)
+ curve:
+ description:
+ - The curve's name for ECC.
+ type: str
+ returned: When C(public_key_type=ECC)
+ exponent_size:
+ description:
+ - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
+ type: int
+ returned: When C(public_key_type=ECC)
+ x:
+ description:
+ - The C(x) coordinate for the public point on the elliptic curve.
+ type: int
+ returned: When C(public_key_type=ECC)
+ y:
+ description:
+ - For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
+ - For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
+ type: int
+ returned: When C(public_key_type=DSA) or C(public_key_type=ECC)
+ public_key_fingerprints:
+ description:
+ - Fingerprints of certificate's public key.
+ - For every hash algorithm available, the fingerprint is computed.
+ returned: success
+ type: dict
+ sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
+ 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
+ fingerprints:
+ description:
+ - Fingerprints of the DER-encoded form of the whole certificate.
+ - For every hash algorithm available, the fingerprint is computed.
+ returned: success
+ type: dict
+ sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
+ 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
+ signature_algorithm:
+ description: The signature algorithm used to sign the certificate.
+ returned: success
+ type: str
+ sample: sha256WithRSAEncryption
+ serial_number:
+ description: The certificate's serial number.
+ returned: success
+ type: int
+ sample: 1234
+ version:
+ description: The certificate version.
+ returned: success
+ type: int
+ sample: 3
+ subject_key_identifier:
+ description:
+ - The certificate's subject key identifier.
+ - The identifier is returned in hexadecimal, with C(:) used to separate bytes.
+ - Is C(none) if the C(SubjectKeyIdentifier) extension is not present.
+ returned: success
+ type: str
+ sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
+ authority_key_identifier:
+ description:
+ - The certificate's authority key identifier.
+ - The identifier is returned in hexadecimal, with C(:) used to separate bytes.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ returned: success
+ type: str
+ sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
+ authority_cert_issuer:
+ description:
+ - The certificate's authority cert issuer as a list of general names.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: list
+ elements: str
+ sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
+ authority_cert_serial_number:
+ description:
+ - The certificate's authority cert serial number.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ returned: success
+ type: int
+ sample: 12345
+ ocsp_uri:
+ description: The OCSP responder URI, if included in the certificate. Will be
+ C(none) if no OCSP responder URI is included.
+ returned: success
+ type: str
+ issuer_uri:
+ description: The Issuer URI, if included in the certificate. Will be
+ C(none) if no issuer URI is included.
+ returned: success
+ type: str
+'''
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils.six import string_types
+from ansible.module_utils.common.text.converters import to_bytes, to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
+ get_certificate_info,
+)
+
+from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock
+
+
+def x509_certificate_info_filter(data, name_encoding='ignore'):
+ '''Extract information from X.509 PEM certificate.'''
+ if not isinstance(data, string_types):
+ raise AnsibleFilterError('The community.crypto.x509_certificate_info input must be a text type, not %s' % type(data))
+ if not isinstance(name_encoding, string_types):
+ raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding))
+ name_encoding = to_native(name_encoding)
+ if name_encoding not in ('ignore', 'idna', 'unicode'):
+ raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding)
+
+ module = FilterModuleMock({'name_encoding': name_encoding})
+ try:
+ return get_certificate_info(module, 'cryptography', content=to_bytes(data))
+ except OpenSSLObjectError as exc:
+ raise AnsibleFilterError(to_native(exc))
+
+
+class FilterModule(object):
+ '''Ansible jinja2 filters'''
+
+ def filters(self):
+ return {
+ 'x509_certificate_info': x509_certificate_info_filter,
+ }
diff --git a/ansible_collections/community/crypto/plugins/filter/x509_crl_info.py b/ansible_collections/community/crypto/plugins/filter/x509_crl_info.py
new file mode 100644
index 00000000..11f61fd8
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/filter/x509_crl_info.py
@@ -0,0 +1,196 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+name: x509_crl_info
+short_description: Retrieve information from X.509 CRLs in PEM format
+version_added: 2.10.0
+author:
+ - Felix Fontein (@felixfontein)
+description:
+ - Provided a X.509 crl in PEM format, retrieve information.
+ - This is a filter version of the M(community.crypto.x509_crl_info) module.
+options:
+ _input:
+ description:
+ - The content of the X.509 CRL in PEM format.
+ type: string
+ required: true
+ list_revoked_certificates:
+ description:
+ - If set to C(false), the list of revoked certificates is not included in the result.
+ - This is useful when retrieving information on large CRL files. Enumerating all revoked
+ certificates can take some time, including serializing the result as JSON, sending it to
+ the Ansible controller, and decoding it again.
+ type: bool
+ default: true
+ version_added: 1.7.0
+extends_documentation_fragment:
+ - community.crypto.name_encoding
+seealso:
+ - module: community.crypto.x509_crl_info
+'''
+
+EXAMPLES = '''
+- name: Show the Organization Name of the CRL's subject
+ ansible.builtin.debug:
+ msg: >-
+ {{
+ (
+ lookup('ansible.builtin.file', '/path/to/cert.pem')
+ | community.crypto.x509_crl_info
+ ).issuer.organizationName
+ }}
+'''
+
+RETURN = '''
+_value:
+ description:
+ - Information on the CRL.
+ type: dict
+ contains:
+ format:
+ description:
+ - Whether the CRL is in PEM format (C(pem)) or in DER format (C(der)).
+ returned: success
+ type: str
+ sample: pem
+ issuer:
+ description:
+ - The CRL's issuer.
+ - Note that for repeated values, only the last one will be returned.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: dict
+ sample: {"organizationName": "Ansible", "commonName": "ca.example.com"}
+ issuer_ordered:
+ description: The CRL's issuer as an ordered list of tuples.
+ returned: success
+ type: list
+ elements: list
+ sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]]
+ last_update:
+ description: The point in time from which this CRL can be trusted as ASN.1 TIME.
+ returned: success
+ type: str
+ sample: '20190413202428Z'
+ next_update:
+ description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME.
+ returned: success
+ type: str
+ sample: '20190413202428Z'
+ digest:
+ description: The signature algorithm used to sign the CRL.
+ returned: success
+ type: str
+ sample: sha256WithRSAEncryption
+ revoked_certificates:
+ description: List of certificates to be revoked.
+ returned: success if I(list_revoked_certificates=true)
+ type: list
+ elements: dict
+ contains:
+ serial_number:
+ description: Serial number of the certificate.
+ type: int
+ sample: 1234
+ revocation_date:
+ description: The point in time the certificate was revoked as ASN.1 TIME.
+ type: str
+ sample: '20190413202428Z'
+ issuer:
+ description:
+ - The certificate's issuer.
+ - See I(name_encoding) for how IDNs are handled.
+ type: list
+ elements: str
+ sample: ["DNS:ca.example.org"]
+ issuer_critical:
+ description: Whether the certificate issuer extension is critical.
+ type: bool
+ sample: false
+ reason:
+ description:
+ - The value for the revocation reason extension.
+ - One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded),
+ C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and
+ C(remove_from_crl).
+ type: str
+ sample: key_compromise
+ reason_critical:
+ description: Whether the revocation reason extension is critical.
+ type: bool
+ sample: false
+ invalidity_date:
+ description: |
+ The point in time it was known/suspected that the private key was compromised
+ or that the certificate otherwise became invalid as ASN.1 TIME.
+ type: str
+ sample: '20190413202428Z'
+ invalidity_date_critical:
+ description: Whether the invalidity date extension is critical.
+ type: bool
+ sample: false
+'''
+
+import base64
+import binascii
+
+from ansible.errors import AnsibleFilterError
+from ansible.module_utils.six import string_types
+from ansible.module_utils.common.text.converters import to_bytes, to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
+ identify_pem_format,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import (
+ get_crl_info,
+)
+
+from ansible_collections.community.crypto.plugins.plugin_utils.filter_module import FilterModuleMock
+
+
+def x509_crl_info_filter(data, name_encoding='ignore', list_revoked_certificates=True):
+ '''Extract information from X.509 PEM certificate.'''
+ if not isinstance(data, string_types):
+ raise AnsibleFilterError('The community.crypto.x509_crl_info input must be a text type, not %s' % type(data))
+ if not isinstance(name_encoding, string_types):
+ raise AnsibleFilterError('The name_encoding option must be of a text type, not %s' % type(name_encoding))
+ if not isinstance(list_revoked_certificates, bool):
+ raise AnsibleFilterError('The list_revoked_certificates option must be a boolean, not %s' % type(list_revoked_certificates))
+ name_encoding = to_native(name_encoding)
+ if name_encoding not in ('ignore', 'idna', 'unicode'):
+ raise AnsibleFilterError('The name_encoding option must be one of the values "ignore", "idna", or "unicode", not "%s"' % name_encoding)
+
+ data = to_bytes(data)
+ if not identify_pem_format(data):
+ try:
+ data = base64.b64decode(to_native(data))
+ except (binascii.Error, TypeError, ValueError, UnicodeEncodeError) as e:
+ pass
+
+ module = FilterModuleMock({'name_encoding': name_encoding})
+ try:
+ return get_crl_info(module, content=data, list_revoked_certificates=list_revoked_certificates)
+ except OpenSSLObjectError as exc:
+ raise AnsibleFilterError(to_native(exc))
+
+
+class FilterModule(object):
+ '''Ansible jinja2 filters'''
+
+ def filters(self):
+ return {
+ 'x509_crl_info': x509_crl_info_filter,
+ }
diff --git a/ansible_collections/community/crypto/plugins/module_utils/_version.py b/ansible_collections/community/crypto/plugins/module_utils/_version.py
new file mode 100644
index 00000000..f7954074
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/_version.py
@@ -0,0 +1,345 @@
+# Vendored copy of distutils/version.py from CPython 3.9.5
+#
+# Implements multiple version numbering conventions for the
+# Python Module Distribution Utilities.
+#
+# Copyright (c) 2001-2022 Python Software Foundation. All rights reserved.
+# PSF License (see LICENSES/PSF-2.0.txt or https://opensource.org/licenses/Python-2.0)
+# SPDX-License-Identifier: PSF-2.0
+#
+
+"""Provides classes to represent module version numbers (one class for
+each style of version numbering). There are currently two such classes
+implemented: StrictVersion and LooseVersion.
+
+Every version number class implements the following interface:
+ * the 'parse' method takes a string and parses it to some internal
+ representation; if the string is an invalid version number,
+ 'parse' raises a ValueError exception
+ * the class constructor takes an optional string argument which,
+ if supplied, is passed to 'parse'
+ * __str__ reconstructs the string that was passed to 'parse' (or
+ an equivalent string -- ie. one that will generate an equivalent
+ version number instance)
+ * __repr__ generates Python code to recreate the version number instance
+ * _cmp compares the current instance with either another instance
+ of the same class or a string (which will be parsed to an instance
+ of the same class, thus must follow the same rules)
+"""
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import re
+
+try:
+ RE_FLAGS = re.VERBOSE | re.ASCII
+except AttributeError:
+ RE_FLAGS = re.VERBOSE
+
+
+class Version:
+ """Abstract base class for version numbering classes. Just provides
+ constructor (__init__) and reproducer (__repr__), because those
+ seem to be the same for all version numbering classes; and route
+ rich comparisons to _cmp.
+ """
+
+ def __init__(self, vstring=None):
+ if vstring:
+ self.parse(vstring)
+
+ def __repr__(self):
+ return "%s ('%s')" % (self.__class__.__name__, str(self))
+
+ def __eq__(self, other):
+ c = self._cmp(other)
+ if c is NotImplemented:
+ return c
+ return c == 0
+
+ def __lt__(self, other):
+ c = self._cmp(other)
+ if c is NotImplemented:
+ return c
+ return c < 0
+
+ def __le__(self, other):
+ c = self._cmp(other)
+ if c is NotImplemented:
+ return c
+ return c <= 0
+
+ def __gt__(self, other):
+ c = self._cmp(other)
+ if c is NotImplemented:
+ return c
+ return c > 0
+
+ def __ge__(self, other):
+ c = self._cmp(other)
+ if c is NotImplemented:
+ return c
+ return c >= 0
+
+
+# Interface for version-number classes -- must be implemented
+# by the following classes (the concrete ones -- Version should
+# be treated as an abstract class).
+# __init__ (string) - create and take same action as 'parse'
+# (string parameter is optional)
+# parse (string) - convert a string representation to whatever
+# internal representation is appropriate for
+# this style of version numbering
+# __str__ (self) - convert back to a string; should be very similar
+# (if not identical to) the string supplied to parse
+# __repr__ (self) - generate Python code to recreate
+# the instance
+# _cmp (self, other) - compare two version numbers ('other' may
+# be an unparsed version string, or another
+# instance of your version class)
+
+
+class StrictVersion(Version):
+ """Version numbering for anal retentives and software idealists.
+ Implements the standard interface for version number classes as
+ described above. A version number consists of two or three
+ dot-separated numeric components, with an optional "pre-release" tag
+ on the end. The pre-release tag consists of the letter 'a' or 'b'
+ followed by a number. If the numeric components of two version
+ numbers are equal, then one with a pre-release tag will always
+ be deemed earlier (lesser) than one without.
+
+ The following are valid version numbers (shown in the order that
+ would be obtained by sorting according to the supplied cmp function):
+
+ 0.4 0.4.0 (these two are equivalent)
+ 0.4.1
+ 0.5a1
+ 0.5b3
+ 0.5
+ 0.9.6
+ 1.0
+ 1.0.4a3
+ 1.0.4b1
+ 1.0.4
+
+ The following are examples of invalid version numbers:
+
+ 1
+ 2.7.2.2
+ 1.3.a4
+ 1.3pl1
+ 1.3c4
+
+ The rationale for this version numbering system will be explained
+ in the distutils documentation.
+ """
+
+ version_re = re.compile(r'^(\d+) \. (\d+) (\. (\d+))? ([ab](\d+))?$',
+ RE_FLAGS)
+
+ def parse(self, vstring):
+ match = self.version_re.match(vstring)
+ if not match:
+ raise ValueError("invalid version number '%s'" % vstring)
+
+ (major, minor, patch, prerelease, prerelease_num) = \
+ match.group(1, 2, 4, 5, 6)
+
+ if patch:
+ self.version = tuple(map(int, [major, minor, patch]))
+ else:
+ self.version = tuple(map(int, [major, minor])) + (0,)
+
+ if prerelease:
+ self.prerelease = (prerelease[0], int(prerelease_num))
+ else:
+ self.prerelease = None
+
+ def __str__(self):
+ if self.version[2] == 0:
+ vstring = '.'.join(map(str, self.version[0:2]))
+ else:
+ vstring = '.'.join(map(str, self.version))
+
+ if self.prerelease:
+ vstring = vstring + self.prerelease[0] + str(self.prerelease[1])
+
+ return vstring
+
+ def _cmp(self, other):
+ if isinstance(other, str):
+ other = StrictVersion(other)
+ elif not isinstance(other, StrictVersion):
+ return NotImplemented
+
+ if self.version != other.version:
+ # numeric versions don't match
+ # prerelease stuff doesn't matter
+ if self.version < other.version:
+ return -1
+ else:
+ return 1
+
+ # have to compare prerelease
+ # case 1: neither has prerelease; they're equal
+ # case 2: self has prerelease, other doesn't; other is greater
+ # case 3: self doesn't have prerelease, other does: self is greater
+ # case 4: both have prerelease: must compare them!
+
+ if (not self.prerelease and not other.prerelease):
+ return 0
+ elif (self.prerelease and not other.prerelease):
+ return -1
+ elif (not self.prerelease and other.prerelease):
+ return 1
+ elif (self.prerelease and other.prerelease):
+ if self.prerelease == other.prerelease:
+ return 0
+ elif self.prerelease < other.prerelease:
+ return -1
+ else:
+ return 1
+ else:
+ raise AssertionError("never get here")
+
+# end class StrictVersion
+
+# The rules according to Greg Stein:
+# 1) a version number has 1 or more numbers separated by a period or by
+# sequences of letters. If only periods, then these are compared
+# left-to-right to determine an ordering.
+# 2) sequences of letters are part of the tuple for comparison and are
+# compared lexicographically
+# 3) recognize the numeric components may have leading zeroes
+#
+# The LooseVersion class below implements these rules: a version number
+# string is split up into a tuple of integer and string components, and
+# comparison is a simple tuple comparison. This means that version
+# numbers behave in a predictable and obvious way, but a way that might
+# not necessarily be how people *want* version numbers to behave. There
+# wouldn't be a problem if people could stick to purely numeric version
+# numbers: just split on period and compare the numbers as tuples.
+# However, people insist on putting letters into their version numbers;
+# the most common purpose seems to be:
+# - indicating a "pre-release" version
+# ('alpha', 'beta', 'a', 'b', 'pre', 'p')
+# - indicating a post-release patch ('p', 'pl', 'patch')
+# but of course this can't cover all version number schemes, and there's
+# no way to know what a programmer means without asking him.
+#
+# The problem is what to do with letters (and other non-numeric
+# characters) in a version number. The current implementation does the
+# obvious and predictable thing: keep them as strings and compare
+# lexically within a tuple comparison. This has the desired effect if
+# an appended letter sequence implies something "post-release":
+# eg. "0.99" < "0.99pl14" < "1.0", and "5.001" < "5.001m" < "5.002".
+#
+# However, if letters in a version number imply a pre-release version,
+# the "obvious" thing isn't correct. Eg. you would expect that
+# "1.5.1" < "1.5.2a2" < "1.5.2", but under the tuple/lexical comparison
+# implemented here, this just isn't so.
+#
+# Two possible solutions come to mind. The first is to tie the
+# comparison algorithm to a particular set of semantic rules, as has
+# been done in the StrictVersion class above. This works great as long
+# as everyone can go along with bondage and discipline. Hopefully a
+# (large) subset of Python module programmers will agree that the
+# particular flavour of bondage and discipline provided by StrictVersion
+# provides enough benefit to be worth using, and will submit their
+# version numbering scheme to its domination. The free-thinking
+# anarchists in the lot will never give in, though, and something needs
+# to be done to accommodate them.
+#
+# Perhaps a "moderately strict" version class could be implemented that
+# lets almost anything slide (syntactically), and makes some heuristic
+# assumptions about non-digits in version number strings. This could
+# sink into special-case-hell, though; if I was as talented and
+# idiosyncratic as Larry Wall, I'd go ahead and implement a class that
+# somehow knows that "1.2.1" < "1.2.2a2" < "1.2.2" < "1.2.2pl3", and is
+# just as happy dealing with things like "2g6" and "1.13++". I don't
+# think I'm smart enough to do it right though.
+#
+# In any case, I've coded the test suite for this module (see
+# ../test/test_version.py) specifically to fail on things like comparing
+# "1.2a2" and "1.2". That's not because the *code* is doing anything
+# wrong, it's because the simple, obvious design doesn't match my
+# complicated, hairy expectations for real-world version numbers. It
+# would be a snap to fix the test suite to say, "Yep, LooseVersion does
+# the Right Thing" (ie. the code matches the conception). But I'd rather
+# have a conception that matches common notions about version numbers.
+
+
+class LooseVersion(Version):
+ """Version numbering for anarchists and software realists.
+ Implements the standard interface for version number classes as
+ described above. A version number consists of a series of numbers,
+ separated by either periods or strings of letters. When comparing
+ version numbers, the numeric components will be compared
+ numerically, and the alphabetic components lexically. The following
+ are all valid version numbers, in no particular order:
+
+ 1.5.1
+ 1.5.2b2
+ 161
+ 3.10a
+ 8.02
+ 3.4j
+ 1996.07.12
+ 3.2.pl0
+ 3.1.1.6
+ 2g6
+ 11g
+ 0.960923
+ 2.2beta29
+ 1.13++
+ 5.5.kw
+ 2.0b1pl0
+
+ In fact, there is no such thing as an invalid version number under
+ this scheme; the rules for comparison are simple and predictable,
+ but may not always give the results you want (for some definition
+ of "want").
+ """
+
+ component_re = re.compile(r'(\d+ | [a-z]+ | \.)', re.VERBOSE)
+
+ def __init__(self, vstring=None):
+ if vstring:
+ self.parse(vstring)
+
+ def parse(self, vstring):
+ # I've given up on thinking I can reconstruct the version string
+ # from the parsed tuple -- so I just store the string here for
+ # use by __str__
+ self.vstring = vstring
+ components = [x for x in self.component_re.split(vstring) if x and x != '.']
+ for i, obj in enumerate(components):
+ try:
+ components[i] = int(obj)
+ except ValueError:
+ pass
+
+ self.version = components
+
+ def __str__(self):
+ return self.vstring
+
+ def __repr__(self):
+ return "LooseVersion ('%s')" % str(self)
+
+ def _cmp(self, other):
+ if isinstance(other, str):
+ other = LooseVersion(other)
+ elif not isinstance(other, LooseVersion):
+ return NotImplemented
+
+ if self.version == other.version:
+ return 0
+ if self.version < other.version:
+ return -1
+ if self.version > other.version:
+ return 1
+
+# end class LooseVersion
diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/account.py b/ansible_collections/community/crypto/plugins/module_utils/acme/account.py
new file mode 100644
index 00000000..97e16498
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/acme/account.py
@@ -0,0 +1,252 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
+ ACMEProtocolException,
+ ModuleFailException,
+)
+
+
+class ACMEAccount(object):
+ '''
+ ACME account object. Allows to create new accounts, check for existence of accounts,
+ retrieve account data.
+ '''
+
+ def __init__(self, client):
+ # Set to true to enable logging of all signed requests
+ self._debug = False
+
+ self.client = client
+
+ def _new_reg(self, contact=None, agreement=None, terms_agreed=False, allow_creation=True,
+ external_account_binding=None):
+ '''
+ Registers a new ACME account. Returns a pair ``(created, data)``.
+ Here, ``created`` is ``True`` if the account was created and
+ ``False`` if it already existed (e.g. it was not newly created),
+ or does not exist. In case the account was created or exists,
+ ``data`` contains the account data; otherwise, it is ``None``.
+
+ If specified, ``external_account_binding`` should be a dictionary
+ with keys ``kid``, ``alg`` and ``key``
+ (https://tools.ietf.org/html/rfc8555#section-7.3.4).
+
+ https://tools.ietf.org/html/rfc8555#section-7.3
+ '''
+ contact = contact or []
+
+ if self.client.version == 1:
+ new_reg = {
+ 'resource': 'new-reg',
+ 'contact': contact
+ }
+ if agreement:
+ new_reg['agreement'] = agreement
+ else:
+ new_reg['agreement'] = self.client.directory['meta']['terms-of-service']
+ if external_account_binding is not None:
+ raise ModuleFailException('External account binding is not supported for ACME v1')
+ url = self.client.directory['new-reg']
+ else:
+ if (external_account_binding is not None or self.client.directory['meta'].get('externalAccountRequired')) and allow_creation:
+ # Some ACME servers such as ZeroSSL do not like it when you try to register an existing account
+ # and provide external_account_binding credentials. Thus we first send a request with allow_creation=False
+ # to see whether the account already exists.
+
+ # Note that we pass contact here: ZeroSSL does not accept regisration calls without contacts, even
+ # if onlyReturnExisting is set to true.
+ created, data = self._new_reg(contact=contact, allow_creation=False)
+ if data:
+ # An account already exists! Return data
+ return created, data
+ # An account does not yet exist. Try to create one next.
+
+ new_reg = {
+ 'contact': contact
+ }
+ if not allow_creation:
+ # https://tools.ietf.org/html/rfc8555#section-7.3.1
+ new_reg['onlyReturnExisting'] = True
+ if terms_agreed:
+ new_reg['termsOfServiceAgreed'] = True
+ url = self.client.directory['newAccount']
+ if external_account_binding is not None:
+ new_reg['externalAccountBinding'] = self.client.sign_request(
+ {
+ 'alg': external_account_binding['alg'],
+ 'kid': external_account_binding['kid'],
+ 'url': url,
+ },
+ self.client.account_jwk,
+ self.client.backend.create_mac_key(external_account_binding['alg'], external_account_binding['key'])
+ )
+ elif self.client.directory['meta'].get('externalAccountRequired') and allow_creation:
+ raise ModuleFailException(
+ 'To create an account, an external account binding must be specified. '
+ 'Use the acme_account module with the external_account_binding option.'
+ )
+
+ result, info = self.client.send_signed_request(url, new_reg, fail_on_error=False)
+
+ if info['status'] in ([200, 201] if self.client.version == 1 else [201]):
+ # Account did not exist
+ if 'location' in info:
+ self.client.set_account_uri(info['location'])
+ return True, result
+ elif info['status'] == (409 if self.client.version == 1 else 200):
+ # Account did exist
+ if result.get('status') == 'deactivated':
+ # A bug in Pebble (https://github.com/letsencrypt/pebble/issues/179) and
+ # Boulder (https://github.com/letsencrypt/boulder/issues/3971): this should
+ # not return a valid account object according to
+ # https://tools.ietf.org/html/rfc8555#section-7.3.6:
+ # "Once an account is deactivated, the server MUST NOT accept further
+ # requests authorized by that account's key."
+ if not allow_creation:
+ return False, None
+ else:
+ raise ModuleFailException("Account is deactivated")
+ if 'location' in info:
+ self.client.set_account_uri(info['location'])
+ return False, result
+ elif info['status'] == 400 and result['type'] == 'urn:ietf:params:acme:error:accountDoesNotExist' and not allow_creation:
+ # Account does not exist (and we did not try to create it)
+ return False, None
+ elif info['status'] == 403 and result['type'] == 'urn:ietf:params:acme:error:unauthorized' and 'deactivated' in (result.get('detail') or ''):
+ # Account has been deactivated; currently works for Pebble; has not been
+ # implemented for Boulder (https://github.com/letsencrypt/boulder/issues/3971),
+ # might need adjustment in error detection.
+ if not allow_creation:
+ return False, None
+ else:
+ raise ModuleFailException("Account is deactivated")
+ else:
+ raise ACMEProtocolException(
+ self.client.module, msg='Registering ACME account failed', info=info, content_json=result)
+
+ def get_account_data(self):
+ '''
+ Retrieve account information. Can only be called when the account
+ URI is already known (such as after calling setup_account).
+ Return None if the account was deactivated, or a dict otherwise.
+ '''
+ if self.client.account_uri is None:
+ raise ModuleFailException("Account URI unknown")
+ if self.client.version == 1:
+ data = {}
+ data['resource'] = 'reg'
+ result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False)
+ else:
+ # try POST-as-GET first (draft-15 or newer)
+ data = None
+ result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False)
+ # check whether that failed with a malformed request error
+ if info['status'] >= 400 and result.get('type') == 'urn:ietf:params:acme:error:malformed':
+ # retry as a regular POST (with no changed data) for pre-draft-15 ACME servers
+ data = {}
+ result, info = self.client.send_signed_request(self.client.account_uri, data, fail_on_error=False)
+ if info['status'] in (400, 403) and result.get('type') == 'urn:ietf:params:acme:error:unauthorized':
+ # Returned when account is deactivated
+ return None
+ if info['status'] in (400, 404) and result.get('type') == 'urn:ietf:params:acme:error:accountDoesNotExist':
+ # Returned when account does not exist
+ return None
+ if info['status'] < 200 or info['status'] >= 300:
+ raise ACMEProtocolException(
+ self.client.module, msg='Error retrieving account data', info=info, content_json=result)
+ return result
+
+ def setup_account(self, contact=None, agreement=None, terms_agreed=False,
+ allow_creation=True, remove_account_uri_if_not_exists=False,
+ external_account_binding=None):
+ '''
+ Detect or create an account on the ACME server. For ACME v1,
+ as the only way (without knowing an account URI) to test if an
+ account exists is to try and create one with the provided account
+ key, this method will always result in an account being present
+ (except on error situations). For ACME v2, a new account will
+ only be created if ``allow_creation`` is set to True.
+
+ For ACME v2, ``check_mode`` is fully respected. For ACME v1, the
+ account might be created if it does not yet exist.
+
+ Return a pair ``(created, account_data)``. Here, ``created`` will
+ be ``True`` in case the account was created or would be created
+ (check mode). ``account_data`` will be the current account data,
+ or ``None`` if the account does not exist.
+
+ The account URI will be stored in ``client.account_uri``; if it is ``None``,
+ the account does not exist.
+
+ If specified, ``external_account_binding`` should be a dictionary
+ with keys ``kid``, ``alg`` and ``key``
+ (https://tools.ietf.org/html/rfc8555#section-7.3.4).
+
+ https://tools.ietf.org/html/rfc8555#section-7.3
+ '''
+
+ if self.client.account_uri is not None:
+ created = False
+ # Verify that the account key belongs to the URI.
+ # (If update_contact is True, this will be done below.)
+ account_data = self.get_account_data()
+ if account_data is None:
+ if remove_account_uri_if_not_exists and not allow_creation:
+ self.client.account_uri = None
+ else:
+ raise ModuleFailException("Account is deactivated or does not exist!")
+ else:
+ created, account_data = self._new_reg(
+ contact,
+ agreement=agreement,
+ terms_agreed=terms_agreed,
+ allow_creation=allow_creation and not self.client.module.check_mode,
+ external_account_binding=external_account_binding,
+ )
+ if self.client.module.check_mode and self.client.account_uri is None and allow_creation:
+ created = True
+ account_data = {
+ 'contact': contact or []
+ }
+ return created, account_data
+
+ def update_account(self, account_data, contact=None):
+ '''
+ Update an account on the ACME server. Check mode is fully respected.
+
+ The current account data must be provided as ``account_data``.
+
+ Return a pair ``(updated, account_data)``, where ``updated`` is
+ ``True`` in case something changed (contact info updated) or
+ would be changed (check mode), and ``account_data`` the updated
+ account data.
+
+ https://tools.ietf.org/html/rfc8555#section-7.3.2
+ '''
+ # Create request
+ update_request = {}
+ if contact is not None and account_data.get('contact', []) != contact:
+ update_request['contact'] = list(contact)
+
+ # No change?
+ if not update_request:
+ return False, dict(account_data)
+
+ # Apply change
+ if self.client.module.check_mode:
+ account_data = dict(account_data)
+ account_data.update(update_request)
+ else:
+ if self.client.version == 1:
+ update_request['resource'] = 'reg'
+ account_data, dummy = self.client.send_signed_request(self.client.account_uri, update_request)
+ return True, account_data
diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/acme.py b/ansible_collections/community/crypto/plugins/module_utils/acme/acme.py
new file mode 100644
index 00000000..c054a52f
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/acme/acme.py
@@ -0,0 +1,452 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import copy
+import datetime
+import json
+import locale
+import time
+import traceback
+
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.common.text.converters import to_bytes
+from ansible.module_utils.urls import fetch_url
+from ansible.module_utils.six import PY3
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.backend_openssl_cli import (
+ OpenSSLCLIBackend,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.backend_cryptography import (
+ CryptographyBackend,
+ CRYPTOGRAPHY_ERROR,
+ CRYPTOGRAPHY_MINIMAL_VERSION,
+ CRYPTOGRAPHY_VERSION,
+ HAS_CURRENT_CRYPTOGRAPHY,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
+ ACMEProtocolException,
+ NetworkException,
+ ModuleFailException,
+ KeyParsingError,
+ format_http_status,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
+ nopad_b64,
+)
+
+try:
+ import ipaddress # noqa: F401, pylint: disable=unused-import
+except ImportError:
+ HAS_IPADDRESS = False
+ IPADDRESS_IMPORT_ERROR = traceback.format_exc()
+else:
+ HAS_IPADDRESS = True
+ IPADDRESS_IMPORT_ERROR = None
+
+
+RETRY_STATUS_CODES = (408, 429, 503)
+
+
+def _decode_retry(module, response, info, retry_count):
+ if info['status'] not in RETRY_STATUS_CODES:
+ return False
+
+ if retry_count >= 5:
+ raise ACMEProtocolException(module, msg='Giving up after 5 retries', info=info, response=response)
+
+ # 429 and 503 should have a Retry-After header (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After)
+ try:
+ retry_after = min(max(1, int(info.get('retry-after'))), 60)
+ except (TypeError, ValueError) as dummy:
+ retry_after = 10
+ module.log('Retrieved a %s HTTP status on %s, retrying in %s seconds' % (format_http_status(info['status']), info['url'], retry_after))
+
+ time.sleep(retry_after)
+ return True
+
+
+def _assert_fetch_url_success(module, response, info, allow_redirect=False, allow_client_error=True, allow_server_error=True):
+ if info['status'] < 0:
+ raise NetworkException(msg="Failure downloading %s, %s" % (info['url'], info['msg']))
+
+ if (300 <= info['status'] < 400 and not allow_redirect) or \
+ (400 <= info['status'] < 500 and not allow_client_error) or \
+ (info['status'] >= 500 and not allow_server_error):
+ raise ACMEProtocolException(module, info=info, response=response)
+
+
+def _is_failed(info, expected_status_codes=None):
+ if info['status'] < 200 or info['status'] >= 400:
+ return True
+ if expected_status_codes is not None and info['status'] not in expected_status_codes:
+ return True
+ return False
+
+
+class ACMEDirectory(object):
+ '''
+ The ACME server directory. Gives access to the available resources,
+ and allows to obtain a Replay-Nonce. The acme_directory URL
+ needs to support unauthenticated GET requests; ACME endpoints
+ requiring authentication are not supported.
+ https://tools.ietf.org/html/rfc8555#section-7.1.1
+ '''
+
+ def __init__(self, module, account):
+ self.module = module
+ self.directory_root = module.params['acme_directory']
+ self.version = module.params['acme_version']
+
+ self.directory, dummy = account.get_request(self.directory_root, get_only=True)
+
+ self.request_timeout = module.params['request_timeout']
+
+ # Check whether self.version matches what we expect
+ if self.version == 1:
+ for key in ('new-reg', 'new-authz', 'new-cert'):
+ if key not in self.directory:
+ raise ModuleFailException("ACME directory does not seem to follow protocol ACME v1")
+ if self.version == 2:
+ for key in ('newNonce', 'newAccount', 'newOrder'):
+ if key not in self.directory:
+ raise ModuleFailException("ACME directory does not seem to follow protocol ACME v2")
+ # Make sure that 'meta' is always available
+ if 'meta' not in self.directory:
+ self.directory['meta'] = {}
+
+ def __getitem__(self, key):
+ return self.directory[key]
+
+ def get_nonce(self, resource=None):
+ url = self.directory_root if self.version == 1 else self.directory['newNonce']
+ if resource is not None:
+ url = resource
+ retry_count = 0
+ while True:
+ response, info = fetch_url(self.module, url, method='HEAD', timeout=self.request_timeout)
+ if _decode_retry(self.module, response, info, retry_count):
+ retry_count += 1
+ continue
+ if info['status'] not in (200, 204):
+ raise NetworkException("Failed to get replay-nonce, got status {0}".format(format_http_status(info['status'])))
+ if 'replay-nonce' in info:
+ return info['replay-nonce']
+ self.module.log(
+ 'HEAD to {0} did return status {1}, but no replay-nonce header!'.format(url, format_http_status(info['status'])))
+ if retry_count >= 5:
+ raise ACMEProtocolException(
+ self.module, msg='Was not able to obtain nonce, giving up after 5 retries', info=info, response=response)
+ retry_count += 1
+
+
+class ACMEClient(object):
+ '''
+ ACME client object. Handles the authorized communication with the
+ ACME server.
+ '''
+
+ def __init__(self, module, backend):
+ # Set to true to enable logging of all signed requests
+ self._debug = False
+
+ self.module = module
+ self.backend = backend
+ self.version = module.params['acme_version']
+ # account_key path and content are mutually exclusive
+ self.account_key_file = module.params['account_key_src']
+ self.account_key_content = module.params['account_key_content']
+ self.account_key_passphrase = module.params['account_key_passphrase']
+
+ # Grab account URI from module parameters.
+ # Make sure empty string is treated as None.
+ self.account_uri = module.params.get('account_uri') or None
+
+ self.request_timeout = module.params['request_timeout']
+
+ self.account_key_data = None
+ self.account_jwk = None
+ self.account_jws_header = None
+ if self.account_key_file is not None or self.account_key_content is not None:
+ try:
+ self.account_key_data = self.parse_key(
+ key_file=self.account_key_file,
+ key_content=self.account_key_content,
+ passphrase=self.account_key_passphrase)
+ except KeyParsingError as e:
+ raise ModuleFailException("Error while parsing account key: {msg}".format(msg=e.msg))
+ self.account_jwk = self.account_key_data['jwk']
+ self.account_jws_header = {
+ "alg": self.account_key_data['alg'],
+ "jwk": self.account_jwk,
+ }
+ if self.account_uri:
+ # Make sure self.account_jws_header is updated
+ self.set_account_uri(self.account_uri)
+
+ self.directory = ACMEDirectory(module, self)
+
+ def set_account_uri(self, uri):
+ '''
+ Set account URI. For ACME v2, it needs to be used to sending signed
+ requests.
+ '''
+ self.account_uri = uri
+ if self.version != 1:
+ self.account_jws_header.pop('jwk')
+ self.account_jws_header['kid'] = self.account_uri
+
+ def parse_key(self, key_file=None, key_content=None, passphrase=None):
+ '''
+ Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
+ In case of an error, raises KeyParsingError.
+ '''
+ if key_file is None and key_content is None:
+ raise AssertionError('One of key_file and key_content must be specified!')
+ return self.backend.parse_key(key_file, key_content, passphrase=passphrase)
+
+ def sign_request(self, protected, payload, key_data, encode_payload=True):
+ '''
+ Signs an ACME request.
+ '''
+ try:
+ if payload is None:
+ # POST-as-GET
+ payload64 = ''
+ else:
+ # POST
+ if encode_payload:
+ payload = self.module.jsonify(payload).encode('utf8')
+ payload64 = nopad_b64(to_bytes(payload))
+ protected64 = nopad_b64(self.module.jsonify(protected).encode('utf8'))
+ except Exception as e:
+ raise ModuleFailException("Failed to encode payload / headers as JSON: {0}".format(e))
+
+ return self.backend.sign(payload64, protected64, key_data)
+
+ def _log(self, msg, data=None):
+ '''
+ Write arguments to acme.log when logging is enabled.
+ '''
+ if self._debug:
+ with open('acme.log', 'ab') as f:
+ f.write('[{0}] {1}\n'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%s'), msg).encode('utf-8'))
+ if data is not None:
+ f.write('{0}\n\n'.format(json.dumps(data, indent=2, sort_keys=True)).encode('utf-8'))
+
+ def send_signed_request(self, url, payload, key_data=None, jws_header=None, parse_json_result=True,
+ encode_payload=True, fail_on_error=True, error_msg=None, expected_status_codes=None):
+ '''
+ Sends a JWS signed HTTP POST request to the ACME server and returns
+ the response as dictionary (if parse_json_result is True) or in raw form
+ (if parse_json_result is False).
+ https://tools.ietf.org/html/rfc8555#section-6.2
+
+ If payload is None, a POST-as-GET is performed.
+ (https://tools.ietf.org/html/rfc8555#section-6.3)
+ '''
+ key_data = key_data or self.account_key_data
+ jws_header = jws_header or self.account_jws_header
+ failed_tries = 0
+ while True:
+ protected = copy.deepcopy(jws_header)
+ protected["nonce"] = self.directory.get_nonce()
+ if self.version != 1:
+ protected["url"] = url
+
+ self._log('URL', url)
+ self._log('protected', protected)
+ self._log('payload', payload)
+ data = self.sign_request(protected, payload, key_data, encode_payload=encode_payload)
+ if self.version == 1:
+ data["header"] = jws_header.copy()
+ for k, v in protected.items():
+ dummy = data["header"].pop(k, None)
+ self._log('signed request', data)
+ data = self.module.jsonify(data)
+
+ headers = {
+ 'Content-Type': 'application/jose+json',
+ }
+ resp, info = fetch_url(self.module, url, data=data, headers=headers, method='POST', timeout=self.request_timeout)
+ if _decode_retry(self.module, resp, info, failed_tries):
+ failed_tries += 1
+ continue
+ _assert_fetch_url_success(self.module, resp, info)
+ result = {}
+
+ try:
+ # In Python 2, reading from a closed response yields a TypeError.
+ # In Python 3, read() simply returns ''
+ if PY3 and resp.closed:
+ raise TypeError
+ content = resp.read()
+ except (AttributeError, TypeError):
+ content = info.pop('body', None)
+
+ if content or not parse_json_result:
+ if (parse_json_result and info['content-type'].startswith('application/json')) or 400 <= info['status'] < 600:
+ try:
+ decoded_result = self.module.from_json(content.decode('utf8'))
+ self._log('parsed result', decoded_result)
+ # In case of badNonce error, try again (up to 5 times)
+ # (https://tools.ietf.org/html/rfc8555#section-6.7)
+ if all((
+ 400 <= info['status'] < 600,
+ decoded_result.get('type') == 'urn:ietf:params:acme:error:badNonce',
+ failed_tries <= 5,
+ )):
+ failed_tries += 1
+ continue
+ if parse_json_result:
+ result = decoded_result
+ else:
+ result = content
+ except ValueError:
+ raise NetworkException("Failed to parse the ACME response: {0} {1}".format(url, content))
+ else:
+ result = content
+
+ if fail_on_error and _is_failed(info, expected_status_codes=expected_status_codes):
+ raise ACMEProtocolException(
+ self.module, msg=error_msg, info=info, content=content, content_json=result if parse_json_result else None)
+ return result, info
+
+ def get_request(self, uri, parse_json_result=True, headers=None, get_only=False,
+ fail_on_error=True, error_msg=None, expected_status_codes=None):
+ '''
+ Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback
+ to GET if server replies with a status code of 405.
+ '''
+ if not get_only and self.version != 1:
+ # Try POST-as-GET
+ content, info = self.send_signed_request(uri, None, parse_json_result=False, fail_on_error=False)
+ if info['status'] == 405:
+ # Instead, do unauthenticated GET
+ get_only = True
+ else:
+ # Do unauthenticated GET
+ get_only = True
+
+ if get_only:
+ # Perform unauthenticated GET
+ retry_count = 0
+ while True:
+ resp, info = fetch_url(self.module, uri, method='GET', headers=headers, timeout=self.request_timeout)
+ if not _decode_retry(self.module, resp, info, retry_count):
+ break
+ retry_count += 1
+
+ _assert_fetch_url_success(self.module, resp, info)
+
+ try:
+ # In Python 2, reading from a closed response yields a TypeError.
+ # In Python 3, read() simply returns ''
+ if PY3 and resp.closed:
+ raise TypeError
+ content = resp.read()
+ except (AttributeError, TypeError):
+ content = info.pop('body', None)
+
+ # Process result
+ parsed_json_result = False
+ if parse_json_result:
+ result = {}
+ if content:
+ if info['content-type'].startswith('application/json'):
+ try:
+ result = self.module.from_json(content.decode('utf8'))
+ parsed_json_result = True
+ except ValueError:
+ raise NetworkException("Failed to parse the ACME response: {0} {1}".format(uri, content))
+ else:
+ result = content
+ else:
+ result = content
+
+ if fail_on_error and _is_failed(info, expected_status_codes=expected_status_codes):
+ raise ACMEProtocolException(
+ self.module, msg=error_msg, info=info, content=content, content_json=result if parsed_json_result else None)
+ return result, info
+
+
+def get_default_argspec():
+ '''
+ Provides default argument spec for the options documented in the acme doc fragment.
+ '''
+ return dict(
+ account_key_src=dict(type='path', aliases=['account_key']),
+ account_key_content=dict(type='str', no_log=True),
+ account_key_passphrase=dict(type='str', no_log=True),
+ account_uri=dict(type='str'),
+ acme_directory=dict(type='str', required=True),
+ acme_version=dict(type='int', required=True, choices=[1, 2]),
+ validate_certs=dict(type='bool', default=True),
+ select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']),
+ request_timeout=dict(type='int', default=10),
+ )
+
+
+def create_backend(module, needs_acme_v2):
+ if not HAS_IPADDRESS:
+ module.fail_json(msg=missing_required_lib('ipaddress'), exception=IPADDRESS_IMPORT_ERROR)
+
+ backend = module.params['select_crypto_backend']
+
+ # Backend autodetect
+ if backend == 'auto':
+ backend = 'cryptography' if HAS_CURRENT_CRYPTOGRAPHY else 'openssl'
+
+ # Create backend object
+ if backend == 'cryptography':
+ if CRYPTOGRAPHY_ERROR is not None:
+ # Either we couldn't import cryptography at all, or there was an unexpected error
+ if CRYPTOGRAPHY_VERSION is None:
+ msg = missing_required_lib('cryptography')
+ else:
+ msg = 'Unexpected error while preparing cryptography: {0}'.format(CRYPTOGRAPHY_ERROR.splitlines()[-1])
+ module.fail_json(msg=msg, exception=CRYPTOGRAPHY_ERROR)
+ if not HAS_CURRENT_CRYPTOGRAPHY:
+ # We succeeded importing cryptography, but its version is too old.
+ module.fail_json(
+ msg='Found cryptography, but only version {0}. {1}'.format(
+ CRYPTOGRAPHY_VERSION,
+ missing_required_lib('cryptography >= {0}'.format(CRYPTOGRAPHY_MINIMAL_VERSION))))
+ module.debug('Using cryptography backend (library version {0})'.format(CRYPTOGRAPHY_VERSION))
+ module_backend = CryptographyBackend(module)
+ elif backend == 'openssl':
+ module.debug('Using OpenSSL binary backend')
+ module_backend = OpenSSLCLIBackend(module)
+ else:
+ module.fail_json(msg='Unknown crypto backend "{0}"!'.format(backend))
+
+ # Check common module parameters
+ if not module.params['validate_certs']:
+ module.warn(
+ 'Disabling certificate validation for communications with ACME endpoint. '
+ 'This should only be done for testing against a local ACME server for '
+ 'development purposes, but *never* for production purposes.'
+ )
+
+ if needs_acme_v2 and module.params['acme_version'] < 2:
+ module.fail_json(msg='The {0} module requires the ACME v2 protocol!'.format(module._name))
+
+ if module.params['acme_version'] == 1:
+ module.deprecate("The value 1 for 'acme_version' is deprecated. Please switch to ACME v2",
+ version='3.0.0', collection_name='community.crypto')
+
+ # AnsibleModule() changes the locale, so change it back to C because we rely
+ # on datetime.datetime.strptime() when parsing certificate dates.
+ locale.setlocale(locale.LC_ALL, 'C')
+
+ return module_backend
diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/backend_cryptography.py b/ansible_collections/community/crypto/plugins/module_utils/acme/backend_cryptography.py
new file mode 100644
index 00000000..207f743f
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/acme/backend_cryptography.py
@@ -0,0 +1,393 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import base64
+import binascii
+import datetime
+import os
+import sys
+import traceback
+
+from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
+ CryptoBackend,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import (
+ ChainMatcher,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
+ BackendException,
+ KeyParsingError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.io import read_file
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ parse_name_field,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
+ cryptography_name_to_oid,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
+ extract_first_pem,
+)
+
+CRYPTOGRAPHY_MINIMAL_VERSION = '1.5'
+
+CRYPTOGRAPHY_ERROR = None
+try:
+ import cryptography
+ import cryptography.hazmat.backends
+ import cryptography.hazmat.primitives.hashes
+ import cryptography.hazmat.primitives.hmac
+ import cryptography.hazmat.primitives.asymmetric.ec
+ import cryptography.hazmat.primitives.asymmetric.padding
+ import cryptography.hazmat.primitives.asymmetric.rsa
+ import cryptography.hazmat.primitives.asymmetric.utils
+ import cryptography.hazmat.primitives.serialization
+ import cryptography.x509
+ import cryptography.x509.oid
+except ImportError as dummy:
+ HAS_CURRENT_CRYPTOGRAPHY = False
+ CRYPTOGRAPHY_VERSION = None
+ CRYPTOGRAPHY_ERROR = traceback.format_exc()
+else:
+ CRYPTOGRAPHY_VERSION = cryptography.__version__
+ HAS_CURRENT_CRYPTOGRAPHY = (LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion(CRYPTOGRAPHY_MINIMAL_VERSION))
+ try:
+ if HAS_CURRENT_CRYPTOGRAPHY:
+ _cryptography_backend = cryptography.hazmat.backends.default_backend()
+ except Exception as dummy:
+ CRYPTOGRAPHY_ERROR = traceback.format_exc()
+
+
+if sys.version_info[0] >= 3:
+ # Python 3 (and newer)
+ def _count_bytes(n):
+ return (n.bit_length() + 7) // 8 if n > 0 else 0
+
+ def _convert_int_to_bytes(count, no):
+ return no.to_bytes(count, byteorder='big')
+
+ def _pad_hex(n, digits):
+ res = hex(n)[2:]
+ if len(res) < digits:
+ res = '0' * (digits - len(res)) + res
+ return res
+else:
+ # Python 2
+ def _count_bytes(n):
+ if n <= 0:
+ return 0
+ h = '%x' % n
+ return (len(h) + 1) // 2
+
+ def _convert_int_to_bytes(count, n):
+ h = '%x' % n
+ if len(h) > 2 * count:
+ raise Exception('Number {1} needs more than {0} bytes!'.format(count, n))
+ return ('0' * (2 * count - len(h)) + h).decode('hex')
+
+ def _pad_hex(n, digits):
+ h = '%x' % n
+ if len(h) < digits:
+ h = '0' * (digits - len(h)) + h
+ return h
+
+
+class CryptographyChainMatcher(ChainMatcher):
+ @staticmethod
+ def _parse_key_identifier(key_identifier, name, criterium_idx, module):
+ if key_identifier:
+ try:
+ return binascii.unhexlify(key_identifier.replace(':', ''))
+ except Exception:
+ if criterium_idx is None:
+ module.warn('Criterium has invalid {0} value. Ignoring criterium.'.format(name))
+ else:
+ module.warn('Criterium {0} in select_chain has invalid {1} value. '
+ 'Ignoring criterium.'.format(criterium_idx, name))
+ return None
+
+ def __init__(self, criterium, module):
+ self.criterium = criterium
+ self.test_certificates = criterium.test_certificates
+ self.subject = []
+ self.issuer = []
+ if criterium.subject:
+ self.subject = [
+ (cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.subject, 'subject')
+ ]
+ if criterium.issuer:
+ self.issuer = [
+ (cryptography_name_to_oid(k), to_native(v)) for k, v in parse_name_field(criterium.issuer, 'issuer')
+ ]
+ self.subject_key_identifier = CryptographyChainMatcher._parse_key_identifier(
+ criterium.subject_key_identifier, 'subject_key_identifier', criterium.index, module)
+ self.authority_key_identifier = CryptographyChainMatcher._parse_key_identifier(
+ criterium.authority_key_identifier, 'authority_key_identifier', criterium.index, module)
+
+ def _match_subject(self, x509_subject, match_subject):
+ for oid, value in match_subject:
+ found = False
+ for attribute in x509_subject:
+ if attribute.oid == oid and value == to_native(attribute.value):
+ found = True
+ break
+ if not found:
+ return False
+ return True
+
+ def match(self, certificate):
+ '''
+ Check whether an alternate chain matches the specified criterium.
+ '''
+ chain = certificate.chain
+ if self.test_certificates == 'last':
+ chain = chain[-1:]
+ elif self.test_certificates == 'first':
+ chain = chain[:1]
+ for cert in chain:
+ try:
+ x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography.hazmat.backends.default_backend())
+ matches = True
+ if not self._match_subject(x509.subject, self.subject):
+ matches = False
+ if not self._match_subject(x509.issuer, self.issuer):
+ matches = False
+ if self.subject_key_identifier:
+ try:
+ ext = x509.extensions.get_extension_for_class(cryptography.x509.SubjectKeyIdentifier)
+ if self.subject_key_identifier != ext.value.digest:
+ matches = False
+ except cryptography.x509.ExtensionNotFound:
+ matches = False
+ if self.authority_key_identifier:
+ try:
+ ext = x509.extensions.get_extension_for_class(cryptography.x509.AuthorityKeyIdentifier)
+ if self.authority_key_identifier != ext.value.key_identifier:
+ matches = False
+ except cryptography.x509.ExtensionNotFound:
+ matches = False
+ if matches:
+ return True
+ except Exception as e:
+ self.module.warn('Error while loading certificate {0}: {1}'.format(cert, e))
+ return False
+
+
+class CryptographyBackend(CryptoBackend):
+ def __init__(self, module):
+ super(CryptographyBackend, self).__init__(module)
+
+ def parse_key(self, key_file=None, key_content=None, passphrase=None):
+ '''
+ Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
+ Raises KeyParsingError in case of errors.
+ '''
+ # If key_content is not given, read key_file
+ if key_content is None:
+ key_content = read_file(key_file)
+ else:
+ key_content = to_bytes(key_content)
+ # Parse key
+ try:
+ key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
+ key_content,
+ password=to_bytes(passphrase) if passphrase is not None else None,
+ backend=_cryptography_backend)
+ except Exception as e:
+ raise KeyParsingError('error while loading key: {0}'.format(e))
+ if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
+ pk = key.public_key().public_numbers()
+ return {
+ 'key_obj': key,
+ 'type': 'rsa',
+ 'alg': 'RS256',
+ 'jwk': {
+ "kty": "RSA",
+ "e": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.e), pk.e)),
+ "n": nopad_b64(_convert_int_to_bytes(_count_bytes(pk.n), pk.n)),
+ },
+ 'hash': 'sha256',
+ }
+ elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
+ pk = key.public_key().public_numbers()
+ if pk.curve.name == 'secp256r1':
+ bits = 256
+ alg = 'ES256'
+ hashalg = 'sha256'
+ point_size = 32
+ curve = 'P-256'
+ elif pk.curve.name == 'secp384r1':
+ bits = 384
+ alg = 'ES384'
+ hashalg = 'sha384'
+ point_size = 48
+ curve = 'P-384'
+ elif pk.curve.name == 'secp521r1':
+ # Not yet supported on Let's Encrypt side, see
+ # https://github.com/letsencrypt/boulder/issues/2217
+ bits = 521
+ alg = 'ES512'
+ hashalg = 'sha512'
+ point_size = 66
+ curve = 'P-521'
+ else:
+ raise KeyParsingError('unknown elliptic curve: {0}'.format(pk.curve.name))
+ num_bytes = (bits + 7) // 8
+ return {
+ 'key_obj': key,
+ 'type': 'ec',
+ 'alg': alg,
+ 'jwk': {
+ "kty": "EC",
+ "crv": curve,
+ "x": nopad_b64(_convert_int_to_bytes(num_bytes, pk.x)),
+ "y": nopad_b64(_convert_int_to_bytes(num_bytes, pk.y)),
+ },
+ 'hash': hashalg,
+ 'point_size': point_size,
+ }
+ else:
+ raise KeyParsingError('unknown key type "{0}"'.format(type(key)))
+
+ def sign(self, payload64, protected64, key_data):
+ sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
+ if 'mac_obj' in key_data:
+ mac = key_data['mac_obj']()
+ mac.update(sign_payload)
+ signature = mac.finalize()
+ elif isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
+ padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15()
+ hashalg = cryptography.hazmat.primitives.hashes.SHA256
+ signature = key_data['key_obj'].sign(sign_payload, padding, hashalg())
+ elif isinstance(key_data['key_obj'], cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
+ if key_data['hash'] == 'sha256':
+ hashalg = cryptography.hazmat.primitives.hashes.SHA256
+ elif key_data['hash'] == 'sha384':
+ hashalg = cryptography.hazmat.primitives.hashes.SHA384
+ elif key_data['hash'] == 'sha512':
+ hashalg = cryptography.hazmat.primitives.hashes.SHA512
+ ecdsa = cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hashalg())
+ r, s = cryptography.hazmat.primitives.asymmetric.utils.decode_dss_signature(key_data['key_obj'].sign(sign_payload, ecdsa))
+ rr = _pad_hex(r, 2 * key_data['point_size'])
+ ss = _pad_hex(s, 2 * key_data['point_size'])
+ signature = binascii.unhexlify(rr) + binascii.unhexlify(ss)
+
+ return {
+ "protected": protected64,
+ "payload": payload64,
+ "signature": nopad_b64(signature),
+ }
+
+ def create_mac_key(self, alg, key):
+ '''Create a MAC key.'''
+ if alg == 'HS256':
+ hashalg = cryptography.hazmat.primitives.hashes.SHA256
+ hashbytes = 32
+ elif alg == 'HS384':
+ hashalg = cryptography.hazmat.primitives.hashes.SHA384
+ hashbytes = 48
+ elif alg == 'HS512':
+ hashalg = cryptography.hazmat.primitives.hashes.SHA512
+ hashbytes = 64
+ else:
+ raise BackendException('Unsupported MAC key algorithm for cryptography backend: {0}'.format(alg))
+ key_bytes = base64.urlsafe_b64decode(key)
+ if len(key_bytes) < hashbytes:
+ raise BackendException(
+ '{0} key must be at least {1} bytes long (after Base64 decoding)'.format(alg, hashbytes))
+ return {
+ 'mac_obj': lambda: cryptography.hazmat.primitives.hmac.HMAC(
+ key_bytes,
+ hashalg(),
+ _cryptography_backend),
+ 'type': 'hmac',
+ 'alg': alg,
+ 'jwk': {
+ 'kty': 'oct',
+ 'k': key,
+ },
+ }
+
+ def get_csr_identifiers(self, csr_filename=None, csr_content=None):
+ '''
+ Return a set of requested identifiers (CN and SANs) for the CSR.
+ Each identifier is a pair (type, identifier), where type is either
+ 'dns' or 'ip'.
+ '''
+ identifiers = set([])
+ if csr_content is None:
+ csr_content = read_file(csr_filename)
+ else:
+ csr_content = to_bytes(csr_content)
+ csr = cryptography.x509.load_pem_x509_csr(csr_content, _cryptography_backend)
+ for sub in csr.subject:
+ if sub.oid == cryptography.x509.oid.NameOID.COMMON_NAME:
+ identifiers.add(('dns', sub.value))
+ for extension in csr.extensions:
+ if extension.oid == cryptography.x509.oid.ExtensionOID.SUBJECT_ALTERNATIVE_NAME:
+ for name in extension.value:
+ if isinstance(name, cryptography.x509.DNSName):
+ identifiers.add(('dns', name.value))
+ elif isinstance(name, cryptography.x509.IPAddress):
+ identifiers.add(('ip', name.value.compressed))
+ else:
+ raise BackendException('Found unsupported SAN identifier {0}'.format(name))
+ return identifiers
+
+ def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
+ '''
+ Return the days the certificate in cert_filename remains valid and -1
+ if the file was not found. If cert_filename contains more than one
+ certificate, only the first one will be considered.
+
+ If now is not specified, datetime.datetime.now() is used.
+ '''
+ if cert_filename is not None:
+ cert_content = None
+ if os.path.exists(cert_filename):
+ cert_content = read_file(cert_filename)
+ else:
+ cert_content = to_bytes(cert_content)
+
+ if cert_content is None:
+ return -1
+
+ # Make sure we have at most one PEM. Otherwise cryptography 36.0.0 will barf.
+ cert_content = to_bytes(extract_first_pem(to_text(cert_content)) or '')
+
+ try:
+ cert = cryptography.x509.load_pem_x509_certificate(cert_content, _cryptography_backend)
+ except Exception as e:
+ if cert_filename is None:
+ raise BackendException('Cannot parse certificate: {0}'.format(e))
+ raise BackendException('Cannot parse certificate {0}: {1}'.format(cert_filename, e))
+
+ if now is None:
+ now = datetime.datetime.now()
+ return (cert.not_valid_after - now).days
+
+ def create_chain_matcher(self, criterium):
+ '''
+ Given a Criterium object, creates a ChainMatcher object.
+ '''
+ return CryptographyChainMatcher(criterium, self.module)
diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/backend_openssl_cli.py b/ansible_collections/community/crypto/plugins/module_utils/acme/backend_openssl_cli.py
new file mode 100644
index 00000000..dabcbdb3
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/acme/backend_openssl_cli.py
@@ -0,0 +1,302 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import base64
+import binascii
+import datetime
+import os
+import re
+import tempfile
+import traceback
+
+from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.backends import (
+ CryptoBackend,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
+ BackendException,
+ KeyParsingError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.utils import nopad_b64
+
+try:
+ import ipaddress
+except ImportError:
+ pass
+
+
+_OPENSSL_ENVIRONMENT_UPDATE = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
+
+
+class OpenSSLCLIBackend(CryptoBackend):
+ def __init__(self, module, openssl_binary=None):
+ super(OpenSSLCLIBackend, self).__init__(module)
+ if openssl_binary is None:
+ openssl_binary = module.get_bin_path('openssl', True)
+ self.openssl_binary = openssl_binary
+
+ def parse_key(self, key_file=None, key_content=None, passphrase=None):
+ '''
+ Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
+ Raises KeyParsingError in case of errors.
+ '''
+ if passphrase is not None:
+ raise KeyParsingError('openssl backend does not support key passphrases')
+ # If key_file is not given, but key_content, write that to a temporary file
+ if key_file is None:
+ 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(key_content.encode('utf-8'))
+ key_file = tmpsrc
+ except Exception as err:
+ try:
+ f.close()
+ except Exception as dummy:
+ pass
+ raise KeyParsingError("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
+ f.close()
+ # Parse key
+ account_key_type = None
+ with open(key_file, "rt") as f:
+ for line in f:
+ m = re.match(r"^\s*-{5,}BEGIN\s+(EC|RSA)\s+PRIVATE\s+KEY-{5,}\s*$", line)
+ if m is not None:
+ account_key_type = m.group(1).lower()
+ break
+ if account_key_type is None:
+ # This happens for example if openssl_privatekey created this key
+ # (as opposed to the OpenSSL binary). For now, we assume this is
+ # an RSA key.
+ # FIXME: add some kind of auto-detection
+ account_key_type = "rsa"
+ if account_key_type not in ("rsa", "ec"):
+ raise KeyParsingError('unknown key type "%s"' % account_key_type)
+
+ openssl_keydump_cmd = [self.openssl_binary, account_key_type, "-in", key_file, "-noout", "-text"]
+ dummy, out, dummy = self.module.run_command(
+ openssl_keydump_cmd, check_rc=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
+
+ if account_key_type == 'rsa':
+ pub_hex, pub_exp = re.search(
+ r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)",
+ to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL).groups()
+ pub_exp = "{0:x}".format(int(pub_exp))
+ if len(pub_exp) % 2:
+ pub_exp = "0{0}".format(pub_exp)
+
+ return {
+ 'key_file': key_file,
+ 'type': 'rsa',
+ 'alg': 'RS256',
+ 'jwk': {
+ "kty": "RSA",
+ "e": nopad_b64(binascii.unhexlify(pub_exp.encode("utf-8"))),
+ "n": nopad_b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))),
+ },
+ 'hash': 'sha256',
+ }
+ elif account_key_type == 'ec':
+ pub_data = re.search(
+ r"pub:\s*\n\s+04:([a-f0-9\:\s]+?)\nASN1 OID: (\S+)(?:\nNIST CURVE: (\S+))?",
+ to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL)
+ if pub_data is None:
+ raise KeyParsingError('cannot parse elliptic curve key')
+ pub_hex = binascii.unhexlify(re.sub(r"(\s|:)", "", pub_data.group(1)).encode("utf-8"))
+ asn1_oid_curve = pub_data.group(2).lower()
+ nist_curve = pub_data.group(3).lower() if pub_data.group(3) else None
+ if asn1_oid_curve == 'prime256v1' or nist_curve == 'p-256':
+ bits = 256
+ alg = 'ES256'
+ hashalg = 'sha256'
+ point_size = 32
+ curve = 'P-256'
+ elif asn1_oid_curve == 'secp384r1' or nist_curve == 'p-384':
+ bits = 384
+ alg = 'ES384'
+ hashalg = 'sha384'
+ point_size = 48
+ curve = 'P-384'
+ elif asn1_oid_curve == 'secp521r1' or nist_curve == 'p-521':
+ # Not yet supported on Let's Encrypt side, see
+ # https://github.com/letsencrypt/boulder/issues/2217
+ bits = 521
+ alg = 'ES512'
+ hashalg = 'sha512'
+ point_size = 66
+ curve = 'P-521'
+ else:
+ raise KeyParsingError('unknown elliptic curve: %s / %s' % (asn1_oid_curve, nist_curve))
+ num_bytes = (bits + 7) // 8
+ if len(pub_hex) != 2 * num_bytes:
+ raise KeyParsingError('bad elliptic curve point (%s / %s)' % (asn1_oid_curve, nist_curve))
+ return {
+ 'key_file': key_file,
+ 'type': 'ec',
+ 'alg': alg,
+ 'jwk': {
+ "kty": "EC",
+ "crv": curve,
+ "x": nopad_b64(pub_hex[:num_bytes]),
+ "y": nopad_b64(pub_hex[num_bytes:]),
+ },
+ 'hash': hashalg,
+ 'point_size': point_size,
+ }
+
+ def sign(self, payload64, protected64, key_data):
+ sign_payload = "{0}.{1}".format(protected64, payload64).encode('utf8')
+ if key_data['type'] == 'hmac':
+ hex_key = to_native(binascii.hexlify(base64.urlsafe_b64decode(key_data['jwk']['k'])))
+ cmd_postfix = ["-mac", "hmac", "-macopt", "hexkey:{0}".format(hex_key), "-binary"]
+ else:
+ cmd_postfix = ["-sign", key_data['key_file']]
+ openssl_sign_cmd = [self.openssl_binary, "dgst", "-{0}".format(key_data['hash'])] + cmd_postfix
+
+ dummy, out, dummy = self.module.run_command(
+ openssl_sign_cmd, data=sign_payload, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
+
+ if key_data['type'] == 'ec':
+ dummy, der_out, dummy = self.module.run_command(
+ [self.openssl_binary, "asn1parse", "-inform", "DER"],
+ data=out, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
+ expected_len = 2 * key_data['point_size']
+ sig = re.findall(
+ r"prim:\s+INTEGER\s+:([0-9A-F]{1,%s})\n" % expected_len,
+ to_text(der_out, errors='surrogate_or_strict'))
+ if len(sig) != 2:
+ raise BackendException(
+ "failed to generate Elliptic Curve signature; cannot parse DER output: {0}".format(
+ to_text(der_out, errors='surrogate_or_strict')))
+ sig[0] = (expected_len - len(sig[0])) * '0' + sig[0]
+ sig[1] = (expected_len - len(sig[1])) * '0' + sig[1]
+ out = binascii.unhexlify(sig[0]) + binascii.unhexlify(sig[1])
+
+ return {
+ "protected": protected64,
+ "payload": payload64,
+ "signature": nopad_b64(to_bytes(out)),
+ }
+
+ def create_mac_key(self, alg, key):
+ '''Create a MAC key.'''
+ if alg == 'HS256':
+ hashalg = 'sha256'
+ hashbytes = 32
+ elif alg == 'HS384':
+ hashalg = 'sha384'
+ hashbytes = 48
+ elif alg == 'HS512':
+ hashalg = 'sha512'
+ hashbytes = 64
+ else:
+ raise BackendException('Unsupported MAC key algorithm for OpenSSL backend: {0}'.format(alg))
+ key_bytes = base64.urlsafe_b64decode(key)
+ if len(key_bytes) < hashbytes:
+ raise BackendException(
+ '{0} key must be at least {1} bytes long (after Base64 decoding)'.format(alg, hashbytes))
+ return {
+ 'type': 'hmac',
+ 'alg': alg,
+ 'jwk': {
+ 'kty': 'oct',
+ 'k': key,
+ },
+ 'hash': hashalg,
+ }
+
+ @staticmethod
+ def _normalize_ip(ip):
+ try:
+ return to_native(ipaddress.ip_address(to_text(ip)).compressed)
+ except ValueError:
+ # We do not want to error out on something IPAddress() cannot parse
+ return ip
+
+ def get_csr_identifiers(self, csr_filename=None, csr_content=None):
+ '''
+ Return a set of requested identifiers (CN and SANs) for the CSR.
+ Each identifier is a pair (type, identifier), where type is either
+ 'dns' or 'ip'.
+ '''
+ filename = csr_filename
+ data = None
+ if csr_content is not None:
+ filename = '/dev/stdin'
+ data = csr_content.encode('utf-8')
+
+ openssl_csr_cmd = [self.openssl_binary, "req", "-in", filename, "-noout", "-text"]
+ dummy, out, dummy = self.module.run_command(
+ openssl_csr_cmd, data=data, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
+
+ identifiers = set([])
+ common_name = re.search(r"Subject:.* CN\s?=\s?([^\s,;/]+)", to_text(out, errors='surrogate_or_strict'))
+ if common_name is not None:
+ identifiers.add(('dns', common_name.group(1)))
+ subject_alt_names = re.search(
+ r"X509v3 Subject Alternative Name: (?:critical)?\n +([^\n]+)\n",
+ to_text(out, errors='surrogate_or_strict'), re.MULTILINE | re.DOTALL)
+ if subject_alt_names is not None:
+ for san in subject_alt_names.group(1).split(", "):
+ if san.lower().startswith("dns:"):
+ identifiers.add(('dns', san[4:]))
+ elif san.lower().startswith("ip:"):
+ identifiers.add(('ip', self._normalize_ip(san[3:])))
+ elif san.lower().startswith("ip address:"):
+ identifiers.add(('ip', self._normalize_ip(san[11:])))
+ else:
+ raise BackendException('Found unsupported SAN identifier "{0}"'.format(san))
+ return identifiers
+
+ def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
+ '''
+ Return the days the certificate in cert_filename remains valid and -1
+ if the file was not found. If cert_filename contains more than one
+ certificate, only the first one will be considered.
+
+ If now is not specified, datetime.datetime.now() is used.
+ '''
+ filename = cert_filename
+ data = None
+ if cert_content is not None:
+ filename = '/dev/stdin'
+ data = cert_content.encode('utf-8')
+ cert_filename_suffix = ''
+ elif cert_filename is not None:
+ if not os.path.exists(cert_filename):
+ return -1
+ cert_filename_suffix = ' in {0}'.format(cert_filename)
+ else:
+ return -1
+
+ openssl_cert_cmd = [self.openssl_binary, "x509", "-in", filename, "-noout", "-text"]
+ dummy, out, dummy = self.module.run_command(
+ openssl_cert_cmd, data=data, check_rc=True, binary_data=True, environ_update=_OPENSSL_ENVIRONMENT_UPDATE)
+ try:
+ not_after_str = re.search(r"\s+Not After\s*:\s+(.*)", to_text(out, errors='surrogate_or_strict')).group(1)
+ not_after = datetime.datetime.strptime(not_after_str, '%b %d %H:%M:%S %Y %Z')
+ except AttributeError:
+ raise BackendException("No 'Not after' date found{0}".format(cert_filename_suffix))
+ except ValueError:
+ raise BackendException("Failed to parse 'Not after' date{0}".format(cert_filename_suffix))
+ if now is None:
+ now = datetime.datetime.now()
+ return (not_after - now).days
+
+ def create_chain_matcher(self, criterium):
+ '''
+ Given a Criterium object, creates a ChainMatcher object.
+ '''
+ raise BackendException('Alternate chain matching can only be used with the "cryptography" backend.')
diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/backends.py b/ansible_collections/community/crypto/plugins/module_utils/acme/backends.py
new file mode 100644
index 00000000..5c48e1a7
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/acme/backends.py
@@ -0,0 +1,59 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+
+from ansible.module_utils import six
+
+
+@six.add_metaclass(abc.ABCMeta)
+class CryptoBackend(object):
+ def __init__(self, module):
+ self.module = module
+
+ @abc.abstractmethod
+ def parse_key(self, key_file=None, key_content=None, passphrase=None):
+ '''
+ Parses an RSA or Elliptic Curve key file in PEM format and returns key_data.
+ Raises KeyParsingError in case of errors.
+ '''
+
+ @abc.abstractmethod
+ def sign(self, payload64, protected64, key_data):
+ pass
+
+ @abc.abstractmethod
+ def create_mac_key(self, alg, key):
+ '''Create a MAC key.'''
+
+ @abc.abstractmethod
+ def get_csr_identifiers(self, csr_filename=None, csr_content=None):
+ '''
+ Return a set of requested identifiers (CN and SANs) for the CSR.
+ Each identifier is a pair (type, identifier), where type is either
+ 'dns' or 'ip'.
+ '''
+
+ @abc.abstractmethod
+ def get_cert_days(self, cert_filename=None, cert_content=None, now=None):
+ '''
+ Return the days the certificate in cert_filename remains valid and -1
+ if the file was not found. If cert_filename contains more than one
+ certificate, only the first one will be considered.
+
+ If now is not specified, datetime.datetime.now() is used.
+ '''
+
+ @abc.abstractmethod
+ def create_chain_matcher(self, criterium):
+ '''
+ Given a Criterium object, creates a ChainMatcher object.
+ '''
diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/certificates.py b/ansible_collections/community/crypto/plugins/module_utils/acme/certificates.py
new file mode 100644
index 00000000..29e5e185
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/acme/certificates.py
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+
+from ansible.module_utils import six
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
+ ModuleFailException,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
+ der_to_pem,
+ nopad_b64,
+ process_links,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
+ split_pem_list,
+)
+
+
+class CertificateChain(object):
+ '''
+ Download and parse the certificate chain.
+ https://tools.ietf.org/html/rfc8555#section-7.4.2
+ '''
+
+ def __init__(self, url):
+ self.url = url
+ self.cert = None
+ self.chain = []
+ self.alternates = []
+
+ @classmethod
+ def download(cls, client, url):
+ content, info = client.get_request(url, parse_json_result=False, headers={'Accept': 'application/pem-certificate-chain'})
+
+ if not content or not info['content-type'].startswith('application/pem-certificate-chain'):
+ raise ModuleFailException(
+ "Cannot download certificate chain from {0}, as content type is not application/pem-certificate-chain: {1} (headers: {2})".format(
+ url, content, info))
+
+ result = cls(url)
+
+ # Parse data
+ certs = split_pem_list(content.decode('utf-8'), keep_inbetween=True)
+ if certs:
+ result.cert = certs[0]
+ result.chain = certs[1:]
+
+ process_links(info, lambda link, relation: result._process_links(client, link, relation))
+
+ if result.cert is None:
+ raise ModuleFailException("Failed to parse certificate chain download from {0}: {1} (headers: {2})".format(url, content, info))
+
+ return result
+
+ def _process_links(self, client, link, relation):
+ if relation == 'up':
+ # Process link-up headers if there was no chain in reply
+ if not self.chain:
+ chain_result, chain_info = client.get_request(link, parse_json_result=False)
+ if chain_info['status'] in [200, 201]:
+ self.chain.append(der_to_pem(chain_result))
+ elif relation == 'alternate':
+ self.alternates.append(link)
+
+ def to_json(self):
+ cert = self.cert.encode('utf8')
+ chain = ('\n'.join(self.chain)).encode('utf8')
+ return {
+ 'cert': cert,
+ 'chain': chain,
+ 'full_chain': cert + chain,
+ }
+
+
+class Criterium(object):
+ def __init__(self, criterium, index=None):
+ self.index = index
+ self.test_certificates = criterium['test_certificates']
+ self.subject = criterium['subject']
+ self.issuer = criterium['issuer']
+ self.subject_key_identifier = criterium['subject_key_identifier']
+ self.authority_key_identifier = criterium['authority_key_identifier']
+
+
+@six.add_metaclass(abc.ABCMeta)
+class ChainMatcher(object):
+ @abc.abstractmethod
+ def match(self, certificate):
+ '''
+ Check whether a certificate chain (CertificateChain instance) matches.
+ '''
+
+
+def retrieve_acme_v1_certificate(client, csr_der):
+ '''
+ Create a new certificate based on the CSR (ACME v1 protocol).
+ Return the certificate object as dict
+ https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
+ '''
+ new_cert = {
+ "resource": "new-cert",
+ "csr": nopad_b64(csr_der),
+ }
+ result, info = client.send_signed_request(
+ client.directory['new-cert'], new_cert, error_msg='Failed to receive certificate', expected_status_codes=[200, 201])
+ cert = CertificateChain(info['location'])
+ cert.cert = der_to_pem(result)
+
+ def f(link, relation):
+ if relation == 'up':
+ chain_result, chain_info = client.get_request(link, parse_json_result=False)
+ if chain_info['status'] in [200, 201]:
+ del cert.chain[:]
+ cert.chain.append(der_to_pem(chain_result))
+
+ process_links(info, f)
+ return cert
diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/challenges.py b/ansible_collections/community/crypto/plugins/module_utils/acme/challenges.py
new file mode 100644
index 00000000..366fde54
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/acme/challenges.py
@@ -0,0 +1,303 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import base64
+import hashlib
+import json
+import re
+import time
+
+from ansible.module_utils.common.text.converters import to_bytes
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
+ nopad_b64,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
+ format_error_problem,
+ ACMEProtocolException,
+ ModuleFailException,
+)
+
+try:
+ import ipaddress
+except ImportError:
+ pass
+
+
+def create_key_authorization(client, token):
+ '''
+ Returns the key authorization for the given token
+ https://tools.ietf.org/html/rfc8555#section-8.1
+ '''
+ accountkey_json = json.dumps(client.account_jwk, sort_keys=True, separators=(',', ':'))
+ thumbprint = nopad_b64(hashlib.sha256(accountkey_json.encode('utf8')).digest())
+ return "{0}.{1}".format(token, thumbprint)
+
+
+def combine_identifier(identifier_type, identifier):
+ return '{type}:{identifier}'.format(type=identifier_type, identifier=identifier)
+
+
+def split_identifier(identifier):
+ parts = identifier.split(':', 1)
+ if len(parts) != 2:
+ raise ModuleFailException(
+ 'Identifier "{identifier}" is not of the form <type>:<identifier>'.format(identifier=identifier))
+ return parts
+
+
+class Challenge(object):
+ def __init__(self, data, url):
+ self.data = data
+
+ self.type = data['type']
+ self.url = url
+ self.status = data['status']
+ self.token = data.get('token')
+
+ @classmethod
+ def from_json(cls, client, data, url=None):
+ return cls(data, url or (data['uri'] if client.version == 1 else data['url']))
+
+ def call_validate(self, client):
+ challenge_response = {}
+ if client.version == 1:
+ token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token)
+ key_authorization = create_key_authorization(client, token)
+ challenge_response['resource'] = 'challenge'
+ challenge_response['keyAuthorization'] = key_authorization
+ challenge_response['type'] = self.type
+ client.send_signed_request(
+ self.url,
+ challenge_response,
+ error_msg='Failed to validate challenge',
+ expected_status_codes=[200, 202],
+ )
+
+ def to_json(self):
+ return self.data.copy()
+
+ def get_validation_data(self, client, identifier_type, identifier):
+ token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token)
+ key_authorization = create_key_authorization(client, token)
+
+ if self.type == 'http-01':
+ # https://tools.ietf.org/html/rfc8555#section-8.3
+ return {
+ 'resource': '.well-known/acme-challenge/{token}'.format(token=token),
+ 'resource_value': key_authorization,
+ }
+
+ if self.type == 'dns-01':
+ if identifier_type != 'dns':
+ return None
+ # https://tools.ietf.org/html/rfc8555#section-8.4
+ resource = '_acme-challenge'
+ value = nopad_b64(hashlib.sha256(to_bytes(key_authorization)).digest())
+ record = (resource + identifier[1:]) if identifier.startswith('*.') else '{0}.{1}'.format(resource, identifier)
+ return {
+ 'resource': resource,
+ 'resource_value': value,
+ 'record': record,
+ }
+
+ if self.type == 'tls-alpn-01':
+ # https://www.rfc-editor.org/rfc/rfc8737.html#section-3
+ if identifier_type == 'ip':
+ # IPv4/IPv6 address: use reverse mapping (RFC1034, RFC3596)
+ resource = ipaddress.ip_address(identifier).reverse_pointer
+ if not resource.endswith('.'):
+ resource += '.'
+ else:
+ resource = identifier
+ value = base64.b64encode(hashlib.sha256(to_bytes(key_authorization)).digest())
+ return {
+ 'resource': resource,
+ 'resource_original': combine_identifier(identifier_type, identifier),
+ 'resource_value': value,
+ }
+
+ # Unknown challenge type: ignore
+ return None
+
+
+class Authorization(object):
+ def _setup(self, client, data):
+ data['uri'] = self.url
+ self.data = data
+ self.challenges = [Challenge.from_json(client, challenge) for challenge in data['challenges']]
+ if client.version == 1 and 'status' not in data:
+ # https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2
+ # "status (required, string): ...
+ # If this field is missing, then the default value is "pending"."
+ self.status = 'pending'
+ else:
+ self.status = data['status']
+ self.identifier = data['identifier']['value']
+ self.identifier_type = data['identifier']['type']
+ if data.get('wildcard', False):
+ self.identifier = '*.{0}'.format(self.identifier)
+
+ def __init__(self, url):
+ self.url = url
+
+ self.data = None
+ self.challenges = []
+ self.status = None
+ self.identifier_type = None
+ self.identifier = None
+
+ @classmethod
+ def from_json(cls, client, data, url):
+ result = cls(url)
+ result._setup(client, data)
+ return result
+
+ @classmethod
+ def from_url(cls, client, url):
+ result = cls(url)
+ result.refresh(client)
+ return result
+
+ @classmethod
+ def create(cls, client, identifier_type, identifier):
+ '''
+ Create a new authorization for the given identifier.
+ Return the authorization object of the new authorization
+ https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.4
+ '''
+ new_authz = {
+ "identifier": {
+ "type": identifier_type,
+ "value": identifier,
+ },
+ }
+ if client.version == 1:
+ url = client.directory['new-authz']
+ new_authz["resource"] = "new-authz"
+ else:
+ if 'newAuthz' not in client.directory.directory:
+ raise ACMEProtocolException(client.module, 'ACME endpoint does not support pre-authorization')
+ url = client.directory['newAuthz']
+
+ result, info = client.send_signed_request(
+ url, new_authz, error_msg='Failed to request challenges', expected_status_codes=[200, 201])
+ return cls.from_json(client, result, info['location'])
+
+ @property
+ def combined_identifier(self):
+ return combine_identifier(self.identifier_type, self.identifier)
+
+ def to_json(self):
+ return self.data.copy()
+
+ def refresh(self, client):
+ result, dummy = client.get_request(self.url)
+ changed = self.data != result
+ self._setup(client, result)
+ return changed
+
+ def get_challenge_data(self, client):
+ '''
+ Returns a dict with the data for all proposed (and supported) challenges
+ of the given authorization.
+ '''
+ data = {}
+ for challenge in self.challenges:
+ validation_data = challenge.get_validation_data(client, self.identifier_type, self.identifier)
+ if validation_data is not None:
+ data[challenge.type] = validation_data
+ return data
+
+ def raise_error(self, error_msg, module=None):
+ '''
+ Aborts with a specific error for a challenge.
+ '''
+ error_details = []
+ # multiple challenges could have failed at this point, gather error
+ # details for all of them before failing
+ for challenge in self.challenges:
+ if challenge.status == 'invalid':
+ msg = 'Challenge {type}'.format(type=challenge.type)
+ if 'error' in challenge.data:
+ msg = '{msg}: {problem}'.format(
+ msg=msg,
+ problem=format_error_problem(challenge.data['error'], subproblem_prefix='{0}.'.format(challenge.type)),
+ )
+ error_details.append(msg)
+ raise ACMEProtocolException(
+ module,
+ 'Failed to validate challenge for {identifier}: {error}. {details}'.format(
+ identifier=self.combined_identifier,
+ error=error_msg,
+ details='; '.join(error_details),
+ ),
+ extras=dict(
+ identifier=self.combined_identifier,
+ authorization=self.data,
+ ),
+ )
+
+ def find_challenge(self, challenge_type):
+ for challenge in self.challenges:
+ if challenge_type == challenge.type:
+ return challenge
+ return None
+
+ def wait_for_validation(self, client, callenge_type):
+ while True:
+ self.refresh(client)
+ if self.status in ['valid', 'invalid', 'revoked']:
+ break
+ time.sleep(2)
+
+ if self.status == 'invalid':
+ self.raise_error('Status is "invalid"', module=client.module)
+
+ return self.status == 'valid'
+
+ def call_validate(self, client, challenge_type, wait=True):
+ '''
+ Validate the authorization provided in the auth dict. Returns True
+ when the validation was successful and False when it was not.
+ '''
+ challenge = self.find_challenge(challenge_type)
+ if challenge is None:
+ raise ModuleFailException('Found no challenge of type "{challenge}" for identifier {identifier}!'.format(
+ challenge=challenge_type,
+ identifier=self.combined_identifier,
+ ))
+
+ challenge.call_validate(client)
+
+ if not wait:
+ return self.status == 'valid'
+ return self.wait_for_validation(client, challenge_type)
+
+ def deactivate(self, client):
+ '''
+ Deactivates this authorization.
+ https://community.letsencrypt.org/t/authorization-deactivation/19860/2
+ https://tools.ietf.org/html/rfc8555#section-7.5.2
+ '''
+ if self.status != 'valid':
+ return
+ authz_deactivate = {
+ 'status': 'deactivated'
+ }
+ if client.version == 1:
+ authz_deactivate['resource'] = 'authz'
+ result, info = client.send_signed_request(self.url, authz_deactivate, fail_on_error=False)
+ if 200 <= info['status'] < 300 and result.get('status') == 'deactivated':
+ self.status = 'deactivated'
+ return True
+ return False
diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/errors.py b/ansible_collections/community/crypto/plugins/module_utils/acme/errors.py
new file mode 100644
index 00000000..208a1ae4
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/acme/errors.py
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from ansible.module_utils.common.text.converters import to_text
+from ansible.module_utils.six import binary_type, PY3
+from ansible.module_utils.six.moves.http_client import responses as http_responses
+
+
+def format_http_status(status_code):
+ expl = http_responses.get(status_code)
+ if not expl:
+ return str(status_code)
+ return '%d %s' % (status_code, expl)
+
+
+def format_error_problem(problem, subproblem_prefix=''):
+ if 'title' in problem:
+ msg = 'Error "{title}" ({type})'.format(
+ type=problem['type'],
+ title=problem['title'],
+ )
+ else:
+ msg = 'Error {type}'.format(type=problem['type'])
+ if 'detail' in problem:
+ msg += ': "{detail}"'.format(detail=problem['detail'])
+ subproblems = problem.get('subproblems')
+ if subproblems is not None:
+ msg = '{msg} Subproblems:'.format(msg=msg)
+ for index, problem in enumerate(subproblems):
+ index_str = '{prefix}{index}'.format(prefix=subproblem_prefix, index=index)
+ msg = '{msg}\n({index}) {problem}'.format(
+ msg=msg,
+ index=index_str,
+ problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index_str)),
+ )
+ return msg
+
+
+class ModuleFailException(Exception):
+ '''
+ If raised, module.fail_json() will be called with the given parameters after cleanup.
+ '''
+ def __init__(self, msg, **args):
+ super(ModuleFailException, self).__init__(self, msg)
+ self.msg = msg
+ self.module_fail_args = args
+
+ def do_fail(self, module, **arguments):
+ module.fail_json(msg=self.msg, other=self.module_fail_args, **arguments)
+
+
+class ACMEProtocolException(ModuleFailException):
+ def __init__(self, module, msg=None, info=None, response=None, content=None, content_json=None, extras=None):
+ # Try to get hold of content, if response is given and content is not provided
+ if content is None and content_json is None and response is not None:
+ try:
+ # In Python 2, reading from a closed response yields a TypeError.
+ # In Python 3, read() simply returns ''
+ if PY3 and response.closed:
+ raise TypeError
+ content = response.read()
+ except (AttributeError, TypeError):
+ content = info.pop('body', None)
+
+ # Make sure that content_json is None or a dictionary
+ if content_json is not None and not isinstance(content_json, dict):
+ if content is None and isinstance(content_json, binary_type):
+ content = content_json
+ content_json = None
+
+ # Try to get hold of JSON decoded content, when content is given and JSON not provided
+ if content_json is None and content is not None and module is not None:
+ try:
+ content_json = module.from_json(to_text(content))
+ except Exception as e:
+ pass
+
+ extras = extras or dict()
+
+ if msg is None:
+ msg = 'ACME request failed'
+ add_msg = ''
+
+ if info is not None:
+ url = info['url']
+ code = info['status']
+ extras['http_url'] = url
+ extras['http_status'] = code
+ if code is not None and code >= 400 and content_json is not None and 'type' in content_json:
+ if 'status' in content_json and content_json['status'] != code:
+ code = 'status {problem_code} (HTTP status: {http_code})'.format(
+ http_code=format_http_status(code), problem_code=content_json['status'])
+ else:
+ code = 'status {problem_code}'.format(problem_code=format_http_status(code))
+ subproblems = content_json.pop('subproblems', None)
+ add_msg = ' {problem}.'.format(problem=format_error_problem(content_json))
+ extras['problem'] = content_json
+ extras['subproblems'] = subproblems or []
+ if subproblems is not None:
+ add_msg = '{add_msg} Subproblems:'.format(add_msg=add_msg)
+ for index, problem in enumerate(subproblems):
+ add_msg = '{add_msg}\n({index}) {problem}.'.format(
+ add_msg=add_msg,
+ index=index,
+ problem=format_error_problem(problem, subproblem_prefix='{0}.'.format(index)),
+ )
+ else:
+ code = 'HTTP status {code}'.format(code=format_http_status(code))
+ if content_json is not None:
+ add_msg = ' The JSON error result: {content}'.format(content=content_json)
+ elif content is not None:
+ add_msg = ' The raw error result: {content}'.format(content=to_text(content))
+ msg = '{msg} for {url} with {code}'.format(msg=msg, url=url, code=format_http_status(code))
+ elif content_json is not None:
+ add_msg = ' The JSON result: {content}'.format(content=content_json)
+ elif content is not None:
+ add_msg = ' The raw result: {content}'.format(content=to_text(content))
+
+ super(ACMEProtocolException, self).__init__(
+ '{msg}.{add_msg}'.format(msg=msg, add_msg=add_msg),
+ **extras
+ )
+ self.problem = {}
+ self.subproblems = []
+ for k, v in extras.items():
+ setattr(self, k, v)
+
+
+class BackendException(ModuleFailException):
+ pass
+
+
+class NetworkException(ModuleFailException):
+ pass
+
+
+class KeyParsingError(ModuleFailException):
+ pass
diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/io.py b/ansible_collections/community/crypto/plugins/module_utils/acme/io.py
new file mode 100644
index 00000000..898d5a3d
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/acme/io.py
@@ -0,0 +1,87 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2013, Romeo Theriault <romeot () hawaii.edu>
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import os
+import shutil
+import tempfile
+import traceback
+
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
+
+
+def read_file(fn, mode='b'):
+ try:
+ with open(fn, 'r' + mode) as f:
+ return f.read()
+ except Exception as e:
+ raise ModuleFailException('Error while reading file "{0}": {1}'.format(fn, e))
+
+
+# This function was adapted from an earlier version of https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/uri.py
+def write_file(module, dest, content):
+ '''
+ Write content to destination file dest, only if the content
+ has changed.
+ '''
+ changed = False
+ # create a tempfile
+ fd, tmpsrc = tempfile.mkstemp(text=False)
+ f = os.fdopen(fd, 'wb')
+ try:
+ f.write(content)
+ except Exception as err:
+ try:
+ f.close()
+ except Exception as dummy:
+ pass
+ os.remove(tmpsrc)
+ raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
+ f.close()
+ checksum_src = None
+ checksum_dest = None
+ # raise an error if there is no tmpsrc file
+ if not os.path.exists(tmpsrc):
+ try:
+ os.remove(tmpsrc)
+ except Exception as dummy:
+ pass
+ raise ModuleFailException("Source %s does not exist" % (tmpsrc))
+ if not os.access(tmpsrc, os.R_OK):
+ os.remove(tmpsrc)
+ raise ModuleFailException("Source %s not readable" % (tmpsrc))
+ checksum_src = module.sha1(tmpsrc)
+ # check if there is no dest file
+ if os.path.exists(dest):
+ # raise an error if copy has no permission on dest
+ if not os.access(dest, os.W_OK):
+ os.remove(tmpsrc)
+ raise ModuleFailException("Destination %s not writable" % (dest))
+ if not os.access(dest, os.R_OK):
+ os.remove(tmpsrc)
+ raise ModuleFailException("Destination %s not readable" % (dest))
+ checksum_dest = module.sha1(dest)
+ else:
+ dirname = os.path.dirname(dest) or '.'
+ if not os.access(dirname, os.W_OK):
+ os.remove(tmpsrc)
+ raise ModuleFailException("Destination dir %s not writable" % (dirname))
+ if checksum_src != checksum_dest:
+ try:
+ shutil.copyfile(tmpsrc, dest)
+ changed = True
+ except Exception as err:
+ os.remove(tmpsrc)
+ raise ModuleFailException("failed to copy %s to %s: %s" % (tmpsrc, dest, to_native(err)), exception=traceback.format_exc())
+ os.remove(tmpsrc)
+ return changed
diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/orders.py b/ansible_collections/community/crypto/plugins/module_utils/acme/orders.py
new file mode 100644
index 00000000..732b430d
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/acme/orders.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import time
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
+ nopad_b64,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
+ ACMEProtocolException,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
+ Authorization,
+)
+
+
+class Order(object):
+ def _setup(self, client, data):
+ self.data = data
+
+ self.status = data['status']
+ self.identifiers = []
+ for identifier in data['identifiers']:
+ self.identifiers.append((identifier['type'], identifier['value']))
+ self.finalize_uri = data.get('finalize')
+ self.certificate_uri = data.get('certificate')
+ self.authorization_uris = data['authorizations']
+ self.authorizations = {}
+
+ def __init__(self, url):
+ self.url = url
+
+ self.data = None
+
+ self.status = None
+ self.identifiers = []
+ self.finalize_uri = None
+ self.certificate_uri = None
+ self.authorization_uris = []
+ self.authorizations = {}
+
+ @classmethod
+ def from_json(cls, client, data, url):
+ result = cls(url)
+ result._setup(client, data)
+ return result
+
+ @classmethod
+ def from_url(cls, client, url):
+ result = cls(url)
+ result.refresh(client)
+ return result
+
+ @classmethod
+ def create(cls, client, identifiers):
+ '''
+ Start a new certificate order (ACME v2 protocol).
+ https://tools.ietf.org/html/rfc8555#section-7.4
+ '''
+ acme_identifiers = []
+ for identifier_type, identifier in identifiers:
+ acme_identifiers.append({
+ 'type': identifier_type,
+ 'value': identifier,
+ })
+ new_order = {
+ "identifiers": acme_identifiers
+ }
+ result, info = client.send_signed_request(
+ client.directory['newOrder'], new_order, error_msg='Failed to start new order', expected_status_codes=[201])
+ return cls.from_json(client, result, info['location'])
+
+ def refresh(self, client):
+ result, dummy = client.get_request(self.url)
+ changed = self.data != result
+ self._setup(client, result)
+ return changed
+
+ def load_authorizations(self, client):
+ for auth_uri in self.authorization_uris:
+ authz = Authorization.from_url(client, auth_uri)
+ self.authorizations[authz.combined_identifier] = authz
+
+ def wait_for_finalization(self, client):
+ while True:
+ self.refresh(client)
+ if self.status in ['valid', 'invalid', 'pending', 'ready']:
+ break
+ time.sleep(2)
+
+ if self.status != 'valid':
+ raise ACMEProtocolException(
+ client.module,
+ 'Failed to wait for order to complete; got status "{status}"'.format(status=self.status),
+ content_json=self.data)
+
+ def finalize(self, client, csr_der, wait=True):
+ '''
+ Create a new certificate based on the csr.
+ Return the certificate object as dict
+ https://tools.ietf.org/html/rfc8555#section-7.4
+ '''
+ new_cert = {
+ "csr": nopad_b64(csr_der),
+ }
+ result, info = client.send_signed_request(
+ self.finalize_uri, new_cert, error_msg='Failed to finalizing order', expected_status_codes=[200])
+ # It is not clear from the RFC whether the finalize call returns the order object or not.
+ # Instead of using the result, we call self.refresh(client) below.
+
+ if wait:
+ self.wait_for_finalization(client)
+ else:
+ self.refresh(client)
+ if self.status not in ['procesing', 'valid', 'invalid']:
+ raise ACMEProtocolException(
+ client.module,
+ 'Failed to finalize order; got status "{status}"'.format(status=self.status),
+ info=info,
+ content_json=result)
diff --git a/ansible_collections/community/crypto/plugins/module_utils/acme/utils.py b/ansible_collections/community/crypto/plugins/module_utils/acme/utils.py
new file mode 100644
index 00000000..217b6de4
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/acme/utils.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import base64
+import re
+import textwrap
+import traceback
+
+from ansible.module_utils.common.text.converters import to_native
+from ansible.module_utils.six.moves.urllib.parse import unquote
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
+
+
+def nopad_b64(data):
+ return base64.urlsafe_b64encode(data).decode('utf8').replace("=", "")
+
+
+def der_to_pem(der_cert):
+ '''
+ Convert the DER format certificate in der_cert to a PEM format certificate and return it.
+ '''
+ return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
+ "\n".join(textwrap.wrap(base64.b64encode(der_cert).decode('utf8'), 64)))
+
+
+def pem_to_der(pem_filename=None, pem_content=None):
+ '''
+ Load PEM file, or use PEM file's content, and convert to DER.
+
+ If PEM contains multiple entities, the first entity will be used.
+ '''
+ certificate_lines = []
+ if pem_content is not None:
+ lines = pem_content.splitlines()
+ elif pem_filename is not None:
+ try:
+ with open(pem_filename, "rt") as f:
+ lines = list(f)
+ except Exception as err:
+ raise ModuleFailException("cannot load PEM file {0}: {1}".format(pem_filename, to_native(err)), exception=traceback.format_exc())
+ else:
+ raise ModuleFailException('One of pem_filename and pem_content must be provided')
+ header_line_count = 0
+ for line in lines:
+ if line.startswith('-----'):
+ header_line_count += 1
+ if header_line_count == 2:
+ # If certificate file contains other certs appended
+ # (like intermediate certificates), ignore these.
+ break
+ continue
+ certificate_lines.append(line.strip())
+ return base64.b64decode(''.join(certificate_lines))
+
+
+def process_links(info, callback):
+ '''
+ Process link header, calls callback for every link header with the URL and relation as options.
+ '''
+ if 'link' in info:
+ link = info['link']
+ for url, relation in re.findall(r'<([^>]+)>;\s*rel="(\w+)"', link):
+ callback(unquote(url), relation)
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/_asn1.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/_asn1.py
new file mode 100644
index 00000000..e99b75a5
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/_asn1.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020, Jordan Borean <jborean93@gmail.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import re
+
+from ansible.module_utils.common.text.converters import to_bytes
+
+
+"""
+An ASN.1 serialized as a string in the OpenSSL format:
+ [modifier,]type[:value]
+
+modifier:
+ The modifier can be 'IMPLICIT:<tag_number><tag_class>,' or 'EXPLICIT:<tag_number><tag_class>' where IMPLICIT
+ changes the tag of the universal value to encode and EXPLICIT prefixes its tag to the existing universal value.
+ The tag_number must be set while the tag_class can be 'U', 'A', 'P', or 'C" for 'Universal', 'Application',
+ 'Private', or 'Context Specific' with C being the default.
+
+type:
+ The underlying ASN.1 type of the value specified. Currently only the following have been implemented:
+ UTF8: The value must be a UTF-8 encoded string.
+
+value:
+ The value to encode, the format of this value depends on the <type> specified.
+"""
+ASN1_STRING_REGEX = re.compile(r'^((?P<tag_type>IMPLICIT|EXPLICIT):(?P<tag_number>\d+)(?P<tag_class>U|A|P|C)?,)?'
+ r'(?P<value_type>[\w\d]+):(?P<value>.*)')
+
+
+class TagClass:
+ universal = 0
+ application = 1
+ context_specific = 2
+ private = 3
+
+
+# Universal tag numbers that can be encoded.
+class TagNumber:
+ utf8_string = 12
+
+
+def _pack_octet_integer(value):
+ """ Packs an integer value into 1 or multiple octets. """
+ # NOTE: This is *NOT* the same as packing an ASN.1 INTEGER like value.
+ octets = bytearray()
+
+ # Continue to shift the number by 7 bits and pack into an octet until the
+ # value is fully packed.
+ while value:
+ octet_value = value & 0b01111111
+
+ # First round (last octet) must have the MSB set.
+ if len(octets):
+ octet_value |= 0b10000000
+
+ octets.append(octet_value)
+ value >>= 7
+
+ # Reverse to ensure the higher order octets are first.
+ octets.reverse()
+ return bytes(octets)
+
+
+def serialize_asn1_string_as_der(value):
+ """ Deserializes an ASN.1 string to a DER encoded byte string. """
+ asn1_match = ASN1_STRING_REGEX.match(value)
+ if not asn1_match:
+ raise ValueError("The ASN.1 serialized string must be in the format [modifier,]type[:value]")
+
+ tag_type = asn1_match.group('tag_type')
+ tag_number = asn1_match.group('tag_number')
+ tag_class = asn1_match.group('tag_class') or 'C'
+ value_type = asn1_match.group('value_type')
+ asn1_value = asn1_match.group('value')
+
+ if value_type != 'UTF8':
+ raise ValueError('The ASN.1 serialized string is not a known type "{0}", only UTF8 types are '
+ 'supported'.format(value_type))
+
+ b_value = to_bytes(asn1_value, encoding='utf-8', errors='surrogate_or_strict')
+
+ # We should only do a universal type tag if not IMPLICITLY tagged or the tag class is not universal.
+ if not tag_type or (tag_type == 'EXPLICIT' and tag_class != 'U'):
+ b_value = pack_asn1(TagClass.universal, False, TagNumber.utf8_string, b_value)
+
+ if tag_type:
+ tag_class = {
+ 'U': TagClass.universal,
+ 'A': TagClass.application,
+ 'P': TagClass.private,
+ 'C': TagClass.context_specific,
+ }[tag_class]
+
+ # When adding support for more types this should be looked into further. For now it works with UTF8Strings.
+ constructed = tag_type == 'EXPLICIT' and tag_class != TagClass.universal
+ b_value = pack_asn1(tag_class, constructed, int(tag_number), b_value)
+
+ return b_value
+
+
+def pack_asn1(tag_class, constructed, tag_number, b_data):
+ """Pack the value into an ASN.1 data structure.
+
+ The structure for an ASN.1 element is
+
+ | Identifier Octet(s) | Length Octet(s) | Data Octet(s) |
+ """
+ b_asn1_data = bytearray()
+
+ if tag_class < 0 or tag_class > 3:
+ raise ValueError("tag_class must be between 0 and 3 not %s" % tag_class)
+
+ # Bit 8 and 7 denotes the class.
+ identifier_octets = tag_class << 6
+ # Bit 6 denotes whether the value is primitive or constructed.
+ identifier_octets |= ((1 if constructed else 0) << 5)
+
+ # Bits 5-1 contain the tag number, if it cannot be encoded in these 5 bits
+ # then they are set and another octet(s) is used to denote the tag number.
+ if tag_number < 31:
+ identifier_octets |= tag_number
+ b_asn1_data.append(identifier_octets)
+ else:
+ identifier_octets |= 31
+ b_asn1_data.append(identifier_octets)
+ b_asn1_data.extend(_pack_octet_integer(tag_number))
+
+ length = len(b_data)
+
+ # If the length can be encoded in 7 bits only 1 octet is required.
+ if length < 128:
+ b_asn1_data.append(length)
+
+ else:
+ # Otherwise the length must be encoded across multiple octets
+ length_octets = bytearray()
+ while length:
+ length_octets.append(length & 0b11111111)
+ length >>= 8
+
+ length_octets.reverse() # Reverse to make the higher octets first.
+
+ # The first length octet must have the MSB set alongside the number of
+ # octets the length was encoded in.
+ b_asn1_data.append(len(length_octets) | 0b10000000)
+ b_asn1_data.extend(length_octets)
+
+ return bytes(b_asn1_data) + b_data
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/_obj2txt.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/_obj2txt.py
new file mode 100644
index 00000000..1ac28367
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/_obj2txt.py
@@ -0,0 +1,57 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is licensed under the
+# Apache 2.0 License. Modules you write using this snippet, which is embedded
+# dynamically by Ansible, still belong to the author of the module, and may assign
+# their own license to the complete work.
+
+# This excerpt is dual licensed under the terms of the Apache License, Version
+# 2.0, and the BSD License. See the LICENSE file at
+# https://github.com/pyca/cryptography/blob/master/LICENSE for complete details.
+#
+# The Apache 2.0 license has been included as LICENSES/Apache-2.0.txt in this collection.
+# The BSD License license has been included as LICENSES/BSD-3-Clause.txt in this collection.
+# SPDX-License-Identifier: Apache-2.0 OR BSD-3-Clause
+#
+# Adapted from cryptography's hazmat/backends/openssl/decode_asn1.py
+#
+# Copyright (c) 2015, 2016 Paul Kehrer (@reaperhulk)
+# Copyright (c) 2017 Fraser Tweedale (@frasertweedale)
+
+# Relevant commits from cryptography project (https://github.com/pyca/cryptography):
+# pyca/cryptography@719d536dd691e84e208534798f2eb4f82aaa2e07
+# pyca/cryptography@5ab6d6a5c05572bd1c75f05baf264a2d0001894a
+# pyca/cryptography@2e776e20eb60378e0af9b7439000d0e80da7c7e3
+# pyca/cryptography@fb309ed24647d1be9e319b61b1f2aa8ebb87b90b
+# pyca/cryptography@2917e460993c475c72d7146c50dc3bbc2414280d
+# pyca/cryptography@3057f91ea9a05fb593825006d87a391286a4d828
+# pyca/cryptography@d607dd7e5bc5c08854ec0c9baff70ba4a35be36f
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+# WARNING: this function no longer works with cryptography 35.0.0 and newer!
+# It must **ONLY** be used in compatibility code for older
+# cryptography versions!
+
+def obj2txt(openssl_lib, openssl_ffi, obj):
+ # Set to 80 on the recommendation of
+ # https://www.openssl.org/docs/crypto/OBJ_nid2ln.html#return_values
+ #
+ # But OIDs longer than this occur in real life (e.g. Active
+ # Directory makes some very long OIDs). So we need to detect
+ # and properly handle the case where the default buffer is not
+ # big enough.
+ #
+ buf_len = 80
+ buf = openssl_ffi.new("char[]", buf_len)
+
+ # 'res' is the number of bytes that *would* be written if the
+ # buffer is large enough. If 'res' > buf_len - 1, we need to
+ # alloc a big-enough buffer and go again.
+ res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1)
+ if res > buf_len - 1: # account for terminating null byte
+ buf_len = res + 1
+ buf = openssl_ffi.new("char[]", buf_len)
+ res = openssl_lib.OBJ_obj2txt(buf, buf_len, obj, 1)
+ return openssl_ffi.buffer(buf, res)[:].decode()
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects.py
new file mode 100644
index 00000000..ed225805
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+from ._objects_data import OID_MAP
+
+OID_LOOKUP = dict()
+NORMALIZE_NAMES = dict()
+NORMALIZE_NAMES_SHORT = dict()
+
+for dotted, names in OID_MAP.items():
+ for name in names:
+ if name in NORMALIZE_NAMES and OID_LOOKUP[name] != dotted:
+ raise AssertionError(
+ 'Name collision during setup: "{0}" for OIDs {1} and {2}'
+ .format(name, dotted, OID_LOOKUP[name])
+ )
+ NORMALIZE_NAMES[name] = names[0]
+ NORMALIZE_NAMES_SHORT[name] = names[-1]
+ OID_LOOKUP[name] = dotted
+for alias, original in [('userID', 'userId')]:
+ if alias in NORMALIZE_NAMES:
+ raise AssertionError(
+ 'Name collision during adding aliases: "{0}" (alias for "{1}") is already mapped to OID {2}'
+ .format(alias, original, OID_LOOKUP[alias])
+ )
+ NORMALIZE_NAMES[alias] = original
+ NORMALIZE_NAMES_SHORT[alias] = NORMALIZE_NAMES_SHORT[original]
+ OID_LOOKUP[alias] = OID_LOOKUP[original]
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects_data.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects_data.py
new file mode 100644
index 00000000..4d57b2ef
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/_objects_data.py
@@ -0,0 +1,1115 @@
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is licensed under the
+# Apache 2.0 License. Modules you write using this snippet, which is embedded
+# dynamically by Ansible, still belong to the author of the module, and may assign
+# their own license to the complete work.
+
+# This has been extracted from the OpenSSL project's objects.txt:
+# https://github.com/openssl/openssl/blob/9537fe5757bb07761fa275d779bbd40bcf5530e4/crypto/objects/objects.txt
+# Extracted with https://gist.github.com/felixfontein/376748017ad65ead093d56a45a5bf376
+
+# In case the following data structure has any copyrightable content, note that it is licensed as follows:
+# Copyright (c) the OpenSSL contributors
+# Licensed under the Apache License 2.0
+# SPDX-License-Identifier: Apache-2.0
+# https://github.com/openssl/openssl/blob/master/LICENSE.txt or LICENSES/Apache-2.0.txt
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+OID_MAP = {
+ '0': ('itu-t', 'ITU-T', 'ccitt'),
+ '0.3.4401.5': ('ntt-ds', ),
+ '0.3.4401.5.3.1.9': ('camellia', ),
+ '0.3.4401.5.3.1.9.1': ('camellia-128-ecb', 'CAMELLIA-128-ECB'),
+ '0.3.4401.5.3.1.9.3': ('camellia-128-ofb', 'CAMELLIA-128-OFB'),
+ '0.3.4401.5.3.1.9.4': ('camellia-128-cfb', 'CAMELLIA-128-CFB'),
+ '0.3.4401.5.3.1.9.6': ('camellia-128-gcm', 'CAMELLIA-128-GCM'),
+ '0.3.4401.5.3.1.9.7': ('camellia-128-ccm', 'CAMELLIA-128-CCM'),
+ '0.3.4401.5.3.1.9.9': ('camellia-128-ctr', 'CAMELLIA-128-CTR'),
+ '0.3.4401.5.3.1.9.10': ('camellia-128-cmac', 'CAMELLIA-128-CMAC'),
+ '0.3.4401.5.3.1.9.21': ('camellia-192-ecb', 'CAMELLIA-192-ECB'),
+ '0.3.4401.5.3.1.9.23': ('camellia-192-ofb', 'CAMELLIA-192-OFB'),
+ '0.3.4401.5.3.1.9.24': ('camellia-192-cfb', 'CAMELLIA-192-CFB'),
+ '0.3.4401.5.3.1.9.26': ('camellia-192-gcm', 'CAMELLIA-192-GCM'),
+ '0.3.4401.5.3.1.9.27': ('camellia-192-ccm', 'CAMELLIA-192-CCM'),
+ '0.3.4401.5.3.1.9.29': ('camellia-192-ctr', 'CAMELLIA-192-CTR'),
+ '0.3.4401.5.3.1.9.30': ('camellia-192-cmac', 'CAMELLIA-192-CMAC'),
+ '0.3.4401.5.3.1.9.41': ('camellia-256-ecb', 'CAMELLIA-256-ECB'),
+ '0.3.4401.5.3.1.9.43': ('camellia-256-ofb', 'CAMELLIA-256-OFB'),
+ '0.3.4401.5.3.1.9.44': ('camellia-256-cfb', 'CAMELLIA-256-CFB'),
+ '0.3.4401.5.3.1.9.46': ('camellia-256-gcm', 'CAMELLIA-256-GCM'),
+ '0.3.4401.5.3.1.9.47': ('camellia-256-ccm', 'CAMELLIA-256-CCM'),
+ '0.3.4401.5.3.1.9.49': ('camellia-256-ctr', 'CAMELLIA-256-CTR'),
+ '0.3.4401.5.3.1.9.50': ('camellia-256-cmac', 'CAMELLIA-256-CMAC'),
+ '0.9': ('data', ),
+ '0.9.2342': ('pss', ),
+ '0.9.2342.19200300': ('ucl', ),
+ '0.9.2342.19200300.100': ('pilot', ),
+ '0.9.2342.19200300.100.1': ('pilotAttributeType', ),
+ '0.9.2342.19200300.100.1.1': ('userId', 'UID'),
+ '0.9.2342.19200300.100.1.2': ('textEncodedORAddress', ),
+ '0.9.2342.19200300.100.1.3': ('rfc822Mailbox', 'mail'),
+ '0.9.2342.19200300.100.1.4': ('info', ),
+ '0.9.2342.19200300.100.1.5': ('favouriteDrink', ),
+ '0.9.2342.19200300.100.1.6': ('roomNumber', ),
+ '0.9.2342.19200300.100.1.7': ('photo', ),
+ '0.9.2342.19200300.100.1.8': ('userClass', ),
+ '0.9.2342.19200300.100.1.9': ('host', ),
+ '0.9.2342.19200300.100.1.10': ('manager', ),
+ '0.9.2342.19200300.100.1.11': ('documentIdentifier', ),
+ '0.9.2342.19200300.100.1.12': ('documentTitle', ),
+ '0.9.2342.19200300.100.1.13': ('documentVersion', ),
+ '0.9.2342.19200300.100.1.14': ('documentAuthor', ),
+ '0.9.2342.19200300.100.1.15': ('documentLocation', ),
+ '0.9.2342.19200300.100.1.20': ('homeTelephoneNumber', ),
+ '0.9.2342.19200300.100.1.21': ('secretary', ),
+ '0.9.2342.19200300.100.1.22': ('otherMailbox', ),
+ '0.9.2342.19200300.100.1.23': ('lastModifiedTime', ),
+ '0.9.2342.19200300.100.1.24': ('lastModifiedBy', ),
+ '0.9.2342.19200300.100.1.25': ('domainComponent', 'DC'),
+ '0.9.2342.19200300.100.1.26': ('aRecord', ),
+ '0.9.2342.19200300.100.1.27': ('pilotAttributeType27', ),
+ '0.9.2342.19200300.100.1.28': ('mXRecord', ),
+ '0.9.2342.19200300.100.1.29': ('nSRecord', ),
+ '0.9.2342.19200300.100.1.30': ('sOARecord', ),
+ '0.9.2342.19200300.100.1.31': ('cNAMERecord', ),
+ '0.9.2342.19200300.100.1.37': ('associatedDomain', ),
+ '0.9.2342.19200300.100.1.38': ('associatedName', ),
+ '0.9.2342.19200300.100.1.39': ('homePostalAddress', ),
+ '0.9.2342.19200300.100.1.40': ('personalTitle', ),
+ '0.9.2342.19200300.100.1.41': ('mobileTelephoneNumber', ),
+ '0.9.2342.19200300.100.1.42': ('pagerTelephoneNumber', ),
+ '0.9.2342.19200300.100.1.43': ('friendlyCountryName', ),
+ '0.9.2342.19200300.100.1.44': ('uniqueIdentifier', 'uid'),
+ '0.9.2342.19200300.100.1.45': ('organizationalStatus', ),
+ '0.9.2342.19200300.100.1.46': ('janetMailbox', ),
+ '0.9.2342.19200300.100.1.47': ('mailPreferenceOption', ),
+ '0.9.2342.19200300.100.1.48': ('buildingName', ),
+ '0.9.2342.19200300.100.1.49': ('dSAQuality', ),
+ '0.9.2342.19200300.100.1.50': ('singleLevelQuality', ),
+ '0.9.2342.19200300.100.1.51': ('subtreeMinimumQuality', ),
+ '0.9.2342.19200300.100.1.52': ('subtreeMaximumQuality', ),
+ '0.9.2342.19200300.100.1.53': ('personalSignature', ),
+ '0.9.2342.19200300.100.1.54': ('dITRedirect', ),
+ '0.9.2342.19200300.100.1.55': ('audio', ),
+ '0.9.2342.19200300.100.1.56': ('documentPublisher', ),
+ '0.9.2342.19200300.100.3': ('pilotAttributeSyntax', ),
+ '0.9.2342.19200300.100.3.4': ('iA5StringSyntax', ),
+ '0.9.2342.19200300.100.3.5': ('caseIgnoreIA5StringSyntax', ),
+ '0.9.2342.19200300.100.4': ('pilotObjectClass', ),
+ '0.9.2342.19200300.100.4.3': ('pilotObject', ),
+ '0.9.2342.19200300.100.4.4': ('pilotPerson', ),
+ '0.9.2342.19200300.100.4.5': ('account', ),
+ '0.9.2342.19200300.100.4.6': ('document', ),
+ '0.9.2342.19200300.100.4.7': ('room', ),
+ '0.9.2342.19200300.100.4.9': ('documentSeries', ),
+ '0.9.2342.19200300.100.4.13': ('Domain', 'domain'),
+ '0.9.2342.19200300.100.4.14': ('rFC822localPart', ),
+ '0.9.2342.19200300.100.4.15': ('dNSDomain', ),
+ '0.9.2342.19200300.100.4.17': ('domainRelatedObject', ),
+ '0.9.2342.19200300.100.4.18': ('friendlyCountry', ),
+ '0.9.2342.19200300.100.4.19': ('simpleSecurityObject', ),
+ '0.9.2342.19200300.100.4.20': ('pilotOrganization', ),
+ '0.9.2342.19200300.100.4.21': ('pilotDSA', ),
+ '0.9.2342.19200300.100.4.22': ('qualityLabelledData', ),
+ '0.9.2342.19200300.100.10': ('pilotGroups', ),
+ '1': ('iso', 'ISO'),
+ '1.0.9797.3.4': ('gmac', 'GMAC'),
+ '1.0.10118.3.0.55': ('whirlpool', ),
+ '1.2': ('ISO Member Body', 'member-body'),
+ '1.2.156': ('ISO CN Member Body', 'ISO-CN'),
+ '1.2.156.10197': ('oscca', ),
+ '1.2.156.10197.1': ('sm-scheme', ),
+ '1.2.156.10197.1.104.1': ('sm4-ecb', 'SM4-ECB'),
+ '1.2.156.10197.1.104.2': ('sm4-cbc', 'SM4-CBC'),
+ '1.2.156.10197.1.104.3': ('sm4-ofb', 'SM4-OFB'),
+ '1.2.156.10197.1.104.4': ('sm4-cfb', 'SM4-CFB'),
+ '1.2.156.10197.1.104.5': ('sm4-cfb1', 'SM4-CFB1'),
+ '1.2.156.10197.1.104.6': ('sm4-cfb8', 'SM4-CFB8'),
+ '1.2.156.10197.1.104.7': ('sm4-ctr', 'SM4-CTR'),
+ '1.2.156.10197.1.301': ('sm2', 'SM2'),
+ '1.2.156.10197.1.401': ('sm3', 'SM3'),
+ '1.2.156.10197.1.501': ('SM2-with-SM3', 'SM2-SM3'),
+ '1.2.156.10197.1.504': ('sm3WithRSAEncryption', 'RSA-SM3'),
+ '1.2.392.200011.61.1.1.1.2': ('camellia-128-cbc', 'CAMELLIA-128-CBC'),
+ '1.2.392.200011.61.1.1.1.3': ('camellia-192-cbc', 'CAMELLIA-192-CBC'),
+ '1.2.392.200011.61.1.1.1.4': ('camellia-256-cbc', 'CAMELLIA-256-CBC'),
+ '1.2.392.200011.61.1.1.3.2': ('id-camellia128-wrap', ),
+ '1.2.392.200011.61.1.1.3.3': ('id-camellia192-wrap', ),
+ '1.2.392.200011.61.1.1.3.4': ('id-camellia256-wrap', ),
+ '1.2.410.200004': ('kisa', 'KISA'),
+ '1.2.410.200004.1.3': ('seed-ecb', 'SEED-ECB'),
+ '1.2.410.200004.1.4': ('seed-cbc', 'SEED-CBC'),
+ '1.2.410.200004.1.5': ('seed-cfb', 'SEED-CFB'),
+ '1.2.410.200004.1.6': ('seed-ofb', 'SEED-OFB'),
+ '1.2.410.200046.1.1': ('aria', ),
+ '1.2.410.200046.1.1.1': ('aria-128-ecb', 'ARIA-128-ECB'),
+ '1.2.410.200046.1.1.2': ('aria-128-cbc', 'ARIA-128-CBC'),
+ '1.2.410.200046.1.1.3': ('aria-128-cfb', 'ARIA-128-CFB'),
+ '1.2.410.200046.1.1.4': ('aria-128-ofb', 'ARIA-128-OFB'),
+ '1.2.410.200046.1.1.5': ('aria-128-ctr', 'ARIA-128-CTR'),
+ '1.2.410.200046.1.1.6': ('aria-192-ecb', 'ARIA-192-ECB'),
+ '1.2.410.200046.1.1.7': ('aria-192-cbc', 'ARIA-192-CBC'),
+ '1.2.410.200046.1.1.8': ('aria-192-cfb', 'ARIA-192-CFB'),
+ '1.2.410.200046.1.1.9': ('aria-192-ofb', 'ARIA-192-OFB'),
+ '1.2.410.200046.1.1.10': ('aria-192-ctr', 'ARIA-192-CTR'),
+ '1.2.410.200046.1.1.11': ('aria-256-ecb', 'ARIA-256-ECB'),
+ '1.2.410.200046.1.1.12': ('aria-256-cbc', 'ARIA-256-CBC'),
+ '1.2.410.200046.1.1.13': ('aria-256-cfb', 'ARIA-256-CFB'),
+ '1.2.410.200046.1.1.14': ('aria-256-ofb', 'ARIA-256-OFB'),
+ '1.2.410.200046.1.1.15': ('aria-256-ctr', 'ARIA-256-CTR'),
+ '1.2.410.200046.1.1.34': ('aria-128-gcm', 'ARIA-128-GCM'),
+ '1.2.410.200046.1.1.35': ('aria-192-gcm', 'ARIA-192-GCM'),
+ '1.2.410.200046.1.1.36': ('aria-256-gcm', 'ARIA-256-GCM'),
+ '1.2.410.200046.1.1.37': ('aria-128-ccm', 'ARIA-128-CCM'),
+ '1.2.410.200046.1.1.38': ('aria-192-ccm', 'ARIA-192-CCM'),
+ '1.2.410.200046.1.1.39': ('aria-256-ccm', 'ARIA-256-CCM'),
+ '1.2.643.2.2': ('cryptopro', ),
+ '1.2.643.2.2.3': ('GOST R 34.11-94 with GOST R 34.10-2001', 'id-GostR3411-94-with-GostR3410-2001'),
+ '1.2.643.2.2.4': ('GOST R 34.11-94 with GOST R 34.10-94', 'id-GostR3411-94-with-GostR3410-94'),
+ '1.2.643.2.2.9': ('GOST R 34.11-94', 'md_gost94'),
+ '1.2.643.2.2.10': ('HMAC GOST 34.11-94', 'id-HMACGostR3411-94'),
+ '1.2.643.2.2.14.0': ('id-Gost28147-89-None-KeyMeshing', ),
+ '1.2.643.2.2.14.1': ('id-Gost28147-89-CryptoPro-KeyMeshing', ),
+ '1.2.643.2.2.19': ('GOST R 34.10-2001', 'gost2001'),
+ '1.2.643.2.2.20': ('GOST R 34.10-94', 'gost94'),
+ '1.2.643.2.2.20.1': ('id-GostR3410-94-a', ),
+ '1.2.643.2.2.20.2': ('id-GostR3410-94-aBis', ),
+ '1.2.643.2.2.20.3': ('id-GostR3410-94-b', ),
+ '1.2.643.2.2.20.4': ('id-GostR3410-94-bBis', ),
+ '1.2.643.2.2.21': ('GOST 28147-89', 'gost89'),
+ '1.2.643.2.2.22': ('GOST 28147-89 MAC', 'gost-mac'),
+ '1.2.643.2.2.23': ('GOST R 34.11-94 PRF', 'prf-gostr3411-94'),
+ '1.2.643.2.2.30.0': ('id-GostR3411-94-TestParamSet', ),
+ '1.2.643.2.2.30.1': ('id-GostR3411-94-CryptoProParamSet', ),
+ '1.2.643.2.2.31.0': ('id-Gost28147-89-TestParamSet', ),
+ '1.2.643.2.2.31.1': ('id-Gost28147-89-CryptoPro-A-ParamSet', ),
+ '1.2.643.2.2.31.2': ('id-Gost28147-89-CryptoPro-B-ParamSet', ),
+ '1.2.643.2.2.31.3': ('id-Gost28147-89-CryptoPro-C-ParamSet', ),
+ '1.2.643.2.2.31.4': ('id-Gost28147-89-CryptoPro-D-ParamSet', ),
+ '1.2.643.2.2.31.5': ('id-Gost28147-89-CryptoPro-Oscar-1-1-ParamSet', ),
+ '1.2.643.2.2.31.6': ('id-Gost28147-89-CryptoPro-Oscar-1-0-ParamSet', ),
+ '1.2.643.2.2.31.7': ('id-Gost28147-89-CryptoPro-RIC-1-ParamSet', ),
+ '1.2.643.2.2.32.0': ('id-GostR3410-94-TestParamSet', ),
+ '1.2.643.2.2.32.2': ('id-GostR3410-94-CryptoPro-A-ParamSet', ),
+ '1.2.643.2.2.32.3': ('id-GostR3410-94-CryptoPro-B-ParamSet', ),
+ '1.2.643.2.2.32.4': ('id-GostR3410-94-CryptoPro-C-ParamSet', ),
+ '1.2.643.2.2.32.5': ('id-GostR3410-94-CryptoPro-D-ParamSet', ),
+ '1.2.643.2.2.33.1': ('id-GostR3410-94-CryptoPro-XchA-ParamSet', ),
+ '1.2.643.2.2.33.2': ('id-GostR3410-94-CryptoPro-XchB-ParamSet', ),
+ '1.2.643.2.2.33.3': ('id-GostR3410-94-CryptoPro-XchC-ParamSet', ),
+ '1.2.643.2.2.35.0': ('id-GostR3410-2001-TestParamSet', ),
+ '1.2.643.2.2.35.1': ('id-GostR3410-2001-CryptoPro-A-ParamSet', ),
+ '1.2.643.2.2.35.2': ('id-GostR3410-2001-CryptoPro-B-ParamSet', ),
+ '1.2.643.2.2.35.3': ('id-GostR3410-2001-CryptoPro-C-ParamSet', ),
+ '1.2.643.2.2.36.0': ('id-GostR3410-2001-CryptoPro-XchA-ParamSet', ),
+ '1.2.643.2.2.36.1': ('id-GostR3410-2001-CryptoPro-XchB-ParamSet', ),
+ '1.2.643.2.2.98': ('GOST R 34.10-2001 DH', 'id-GostR3410-2001DH'),
+ '1.2.643.2.2.99': ('GOST R 34.10-94 DH', 'id-GostR3410-94DH'),
+ '1.2.643.2.9': ('cryptocom', ),
+ '1.2.643.2.9.1.3.3': ('GOST R 34.11-94 with GOST R 34.10-94 Cryptocom', 'id-GostR3411-94-with-GostR3410-94-cc'),
+ '1.2.643.2.9.1.3.4': ('GOST R 34.11-94 with GOST R 34.10-2001 Cryptocom', 'id-GostR3411-94-with-GostR3410-2001-cc'),
+ '1.2.643.2.9.1.5.3': ('GOST 34.10-94 Cryptocom', 'gost94cc'),
+ '1.2.643.2.9.1.5.4': ('GOST 34.10-2001 Cryptocom', 'gost2001cc'),
+ '1.2.643.2.9.1.6.1': ('GOST 28147-89 Cryptocom ParamSet', 'id-Gost28147-89-cc'),
+ '1.2.643.2.9.1.8.1': ('GOST R 3410-2001 Parameter Set Cryptocom', 'id-GostR3410-2001-ParamSet-cc'),
+ '1.2.643.3.131.1.1': ('INN', 'INN'),
+ '1.2.643.7.1': ('id-tc26', ),
+ '1.2.643.7.1.1': ('id-tc26-algorithms', ),
+ '1.2.643.7.1.1.1': ('id-tc26-sign', ),
+ '1.2.643.7.1.1.1.1': ('GOST R 34.10-2012 with 256 bit modulus', 'gost2012_256'),
+ '1.2.643.7.1.1.1.2': ('GOST R 34.10-2012 with 512 bit modulus', 'gost2012_512'),
+ '1.2.643.7.1.1.2': ('id-tc26-digest', ),
+ '1.2.643.7.1.1.2.2': ('GOST R 34.11-2012 with 256 bit hash', 'md_gost12_256'),
+ '1.2.643.7.1.1.2.3': ('GOST R 34.11-2012 with 512 bit hash', 'md_gost12_512'),
+ '1.2.643.7.1.1.3': ('id-tc26-signwithdigest', ),
+ '1.2.643.7.1.1.3.2': ('GOST R 34.10-2012 with GOST R 34.11-2012 (256 bit)', 'id-tc26-signwithdigest-gost3410-2012-256'),
+ '1.2.643.7.1.1.3.3': ('GOST R 34.10-2012 with GOST R 34.11-2012 (512 bit)', 'id-tc26-signwithdigest-gost3410-2012-512'),
+ '1.2.643.7.1.1.4': ('id-tc26-mac', ),
+ '1.2.643.7.1.1.4.1': ('HMAC GOST 34.11-2012 256 bit', 'id-tc26-hmac-gost-3411-2012-256'),
+ '1.2.643.7.1.1.4.2': ('HMAC GOST 34.11-2012 512 bit', 'id-tc26-hmac-gost-3411-2012-512'),
+ '1.2.643.7.1.1.5': ('id-tc26-cipher', ),
+ '1.2.643.7.1.1.5.1': ('id-tc26-cipher-gostr3412-2015-magma', ),
+ '1.2.643.7.1.1.5.1.1': ('id-tc26-cipher-gostr3412-2015-magma-ctracpkm', ),
+ '1.2.643.7.1.1.5.1.2': ('id-tc26-cipher-gostr3412-2015-magma-ctracpkm-omac', ),
+ '1.2.643.7.1.1.5.2': ('id-tc26-cipher-gostr3412-2015-kuznyechik', ),
+ '1.2.643.7.1.1.5.2.1': ('id-tc26-cipher-gostr3412-2015-kuznyechik-ctracpkm', ),
+ '1.2.643.7.1.1.5.2.2': ('id-tc26-cipher-gostr3412-2015-kuznyechik-ctracpkm-omac', ),
+ '1.2.643.7.1.1.6': ('id-tc26-agreement', ),
+ '1.2.643.7.1.1.6.1': ('id-tc26-agreement-gost-3410-2012-256', ),
+ '1.2.643.7.1.1.6.2': ('id-tc26-agreement-gost-3410-2012-512', ),
+ '1.2.643.7.1.1.7': ('id-tc26-wrap', ),
+ '1.2.643.7.1.1.7.1': ('id-tc26-wrap-gostr3412-2015-magma', ),
+ '1.2.643.7.1.1.7.1.1': ('id-tc26-wrap-gostr3412-2015-magma-kexp15', 'id-tc26-wrap-gostr3412-2015-kuznyechik-kexp15'),
+ '1.2.643.7.1.1.7.2': ('id-tc26-wrap-gostr3412-2015-kuznyechik', ),
+ '1.2.643.7.1.2': ('id-tc26-constants', ),
+ '1.2.643.7.1.2.1': ('id-tc26-sign-constants', ),
+ '1.2.643.7.1.2.1.1': ('id-tc26-gost-3410-2012-256-constants', ),
+ '1.2.643.7.1.2.1.1.1': ('GOST R 34.10-2012 (256 bit) ParamSet A', 'id-tc26-gost-3410-2012-256-paramSetA'),
+ '1.2.643.7.1.2.1.1.2': ('GOST R 34.10-2012 (256 bit) ParamSet B', 'id-tc26-gost-3410-2012-256-paramSetB'),
+ '1.2.643.7.1.2.1.1.3': ('GOST R 34.10-2012 (256 bit) ParamSet C', 'id-tc26-gost-3410-2012-256-paramSetC'),
+ '1.2.643.7.1.2.1.1.4': ('GOST R 34.10-2012 (256 bit) ParamSet D', 'id-tc26-gost-3410-2012-256-paramSetD'),
+ '1.2.643.7.1.2.1.2': ('id-tc26-gost-3410-2012-512-constants', ),
+ '1.2.643.7.1.2.1.2.0': ('GOST R 34.10-2012 (512 bit) testing parameter set', 'id-tc26-gost-3410-2012-512-paramSetTest'),
+ '1.2.643.7.1.2.1.2.1': ('GOST R 34.10-2012 (512 bit) ParamSet A', 'id-tc26-gost-3410-2012-512-paramSetA'),
+ '1.2.643.7.1.2.1.2.2': ('GOST R 34.10-2012 (512 bit) ParamSet B', 'id-tc26-gost-3410-2012-512-paramSetB'),
+ '1.2.643.7.1.2.1.2.3': ('GOST R 34.10-2012 (512 bit) ParamSet C', 'id-tc26-gost-3410-2012-512-paramSetC'),
+ '1.2.643.7.1.2.2': ('id-tc26-digest-constants', ),
+ '1.2.643.7.1.2.5': ('id-tc26-cipher-constants', ),
+ '1.2.643.7.1.2.5.1': ('id-tc26-gost-28147-constants', ),
+ '1.2.643.7.1.2.5.1.1': ('GOST 28147-89 TC26 parameter set', 'id-tc26-gost-28147-param-Z'),
+ '1.2.643.100.1': ('OGRN', 'OGRN'),
+ '1.2.643.100.3': ('SNILS', 'SNILS'),
+ '1.2.643.100.111': ('Signing Tool of Subject', 'subjectSignTool'),
+ '1.2.643.100.112': ('Signing Tool of Issuer', 'issuerSignTool'),
+ '1.2.804': ('ISO-UA', ),
+ '1.2.804.2.1.1.1': ('ua-pki', ),
+ '1.2.804.2.1.1.1.1.1.1': ('DSTU Gost 28147-2009', 'dstu28147'),
+ '1.2.804.2.1.1.1.1.1.1.2': ('DSTU Gost 28147-2009 OFB mode', 'dstu28147-ofb'),
+ '1.2.804.2.1.1.1.1.1.1.3': ('DSTU Gost 28147-2009 CFB mode', 'dstu28147-cfb'),
+ '1.2.804.2.1.1.1.1.1.1.5': ('DSTU Gost 28147-2009 key wrap', 'dstu28147-wrap'),
+ '1.2.804.2.1.1.1.1.1.2': ('HMAC DSTU Gost 34311-95', 'hmacWithDstu34311'),
+ '1.2.804.2.1.1.1.1.2.1': ('DSTU Gost 34311-95', 'dstu34311'),
+ '1.2.804.2.1.1.1.1.3.1.1': ('DSTU 4145-2002 little endian', 'dstu4145le'),
+ '1.2.804.2.1.1.1.1.3.1.1.1.1': ('DSTU 4145-2002 big endian', 'dstu4145be'),
+ '1.2.804.2.1.1.1.1.3.1.1.2.0': ('DSTU curve 0', 'uacurve0'),
+ '1.2.804.2.1.1.1.1.3.1.1.2.1': ('DSTU curve 1', 'uacurve1'),
+ '1.2.804.2.1.1.1.1.3.1.1.2.2': ('DSTU curve 2', 'uacurve2'),
+ '1.2.804.2.1.1.1.1.3.1.1.2.3': ('DSTU curve 3', 'uacurve3'),
+ '1.2.804.2.1.1.1.1.3.1.1.2.4': ('DSTU curve 4', 'uacurve4'),
+ '1.2.804.2.1.1.1.1.3.1.1.2.5': ('DSTU curve 5', 'uacurve5'),
+ '1.2.804.2.1.1.1.1.3.1.1.2.6': ('DSTU curve 6', 'uacurve6'),
+ '1.2.804.2.1.1.1.1.3.1.1.2.7': ('DSTU curve 7', 'uacurve7'),
+ '1.2.804.2.1.1.1.1.3.1.1.2.8': ('DSTU curve 8', 'uacurve8'),
+ '1.2.804.2.1.1.1.1.3.1.1.2.9': ('DSTU curve 9', 'uacurve9'),
+ '1.2.840': ('ISO US Member Body', 'ISO-US'),
+ '1.2.840.10040': ('X9.57', 'X9-57'),
+ '1.2.840.10040.2': ('holdInstruction', ),
+ '1.2.840.10040.2.1': ('Hold Instruction None', 'holdInstructionNone'),
+ '1.2.840.10040.2.2': ('Hold Instruction Call Issuer', 'holdInstructionCallIssuer'),
+ '1.2.840.10040.2.3': ('Hold Instruction Reject', 'holdInstructionReject'),
+ '1.2.840.10040.4': ('X9.57 CM ?', 'X9cm'),
+ '1.2.840.10040.4.1': ('dsaEncryption', 'DSA'),
+ '1.2.840.10040.4.3': ('dsaWithSHA1', 'DSA-SHA1'),
+ '1.2.840.10045': ('ANSI X9.62', 'ansi-X9-62'),
+ '1.2.840.10045.1': ('id-fieldType', ),
+ '1.2.840.10045.1.1': ('prime-field', ),
+ '1.2.840.10045.1.2': ('characteristic-two-field', ),
+ '1.2.840.10045.1.2.3': ('id-characteristic-two-basis', ),
+ '1.2.840.10045.1.2.3.1': ('onBasis', ),
+ '1.2.840.10045.1.2.3.2': ('tpBasis', ),
+ '1.2.840.10045.1.2.3.3': ('ppBasis', ),
+ '1.2.840.10045.2': ('id-publicKeyType', ),
+ '1.2.840.10045.2.1': ('id-ecPublicKey', ),
+ '1.2.840.10045.3': ('ellipticCurve', ),
+ '1.2.840.10045.3.0': ('c-TwoCurve', ),
+ '1.2.840.10045.3.0.1': ('c2pnb163v1', ),
+ '1.2.840.10045.3.0.2': ('c2pnb163v2', ),
+ '1.2.840.10045.3.0.3': ('c2pnb163v3', ),
+ '1.2.840.10045.3.0.4': ('c2pnb176v1', ),
+ '1.2.840.10045.3.0.5': ('c2tnb191v1', ),
+ '1.2.840.10045.3.0.6': ('c2tnb191v2', ),
+ '1.2.840.10045.3.0.7': ('c2tnb191v3', ),
+ '1.2.840.10045.3.0.8': ('c2onb191v4', ),
+ '1.2.840.10045.3.0.9': ('c2onb191v5', ),
+ '1.2.840.10045.3.0.10': ('c2pnb208w1', ),
+ '1.2.840.10045.3.0.11': ('c2tnb239v1', ),
+ '1.2.840.10045.3.0.12': ('c2tnb239v2', ),
+ '1.2.840.10045.3.0.13': ('c2tnb239v3', ),
+ '1.2.840.10045.3.0.14': ('c2onb239v4', ),
+ '1.2.840.10045.3.0.15': ('c2onb239v5', ),
+ '1.2.840.10045.3.0.16': ('c2pnb272w1', ),
+ '1.2.840.10045.3.0.17': ('c2pnb304w1', ),
+ '1.2.840.10045.3.0.18': ('c2tnb359v1', ),
+ '1.2.840.10045.3.0.19': ('c2pnb368w1', ),
+ '1.2.840.10045.3.0.20': ('c2tnb431r1', ),
+ '1.2.840.10045.3.1': ('primeCurve', ),
+ '1.2.840.10045.3.1.1': ('prime192v1', ),
+ '1.2.840.10045.3.1.2': ('prime192v2', ),
+ '1.2.840.10045.3.1.3': ('prime192v3', ),
+ '1.2.840.10045.3.1.4': ('prime239v1', ),
+ '1.2.840.10045.3.1.5': ('prime239v2', ),
+ '1.2.840.10045.3.1.6': ('prime239v3', ),
+ '1.2.840.10045.3.1.7': ('prime256v1', ),
+ '1.2.840.10045.4': ('id-ecSigType', ),
+ '1.2.840.10045.4.1': ('ecdsa-with-SHA1', ),
+ '1.2.840.10045.4.2': ('ecdsa-with-Recommended', ),
+ '1.2.840.10045.4.3': ('ecdsa-with-Specified', ),
+ '1.2.840.10045.4.3.1': ('ecdsa-with-SHA224', ),
+ '1.2.840.10045.4.3.2': ('ecdsa-with-SHA256', ),
+ '1.2.840.10045.4.3.3': ('ecdsa-with-SHA384', ),
+ '1.2.840.10045.4.3.4': ('ecdsa-with-SHA512', ),
+ '1.2.840.10046.2.1': ('X9.42 DH', 'dhpublicnumber'),
+ '1.2.840.113533.7.66.10': ('cast5-cbc', 'CAST5-CBC'),
+ '1.2.840.113533.7.66.12': ('pbeWithMD5AndCast5CBC', ),
+ '1.2.840.113533.7.66.13': ('password based MAC', 'id-PasswordBasedMAC'),
+ '1.2.840.113533.7.66.30': ('Diffie-Hellman based MAC', 'id-DHBasedMac'),
+ '1.2.840.113549': ('RSA Data Security, Inc.', 'rsadsi'),
+ '1.2.840.113549.1': ('RSA Data Security, Inc. PKCS', 'pkcs'),
+ '1.2.840.113549.1.1': ('pkcs1', ),
+ '1.2.840.113549.1.1.1': ('rsaEncryption', ),
+ '1.2.840.113549.1.1.2': ('md2WithRSAEncryption', 'RSA-MD2'),
+ '1.2.840.113549.1.1.3': ('md4WithRSAEncryption', 'RSA-MD4'),
+ '1.2.840.113549.1.1.4': ('md5WithRSAEncryption', 'RSA-MD5'),
+ '1.2.840.113549.1.1.5': ('sha1WithRSAEncryption', 'RSA-SHA1'),
+ '1.2.840.113549.1.1.6': ('rsaOAEPEncryptionSET', ),
+ '1.2.840.113549.1.1.7': ('rsaesOaep', 'RSAES-OAEP'),
+ '1.2.840.113549.1.1.8': ('mgf1', 'MGF1'),
+ '1.2.840.113549.1.1.9': ('pSpecified', 'PSPECIFIED'),
+ '1.2.840.113549.1.1.10': ('rsassaPss', 'RSASSA-PSS'),
+ '1.2.840.113549.1.1.11': ('sha256WithRSAEncryption', 'RSA-SHA256'),
+ '1.2.840.113549.1.1.12': ('sha384WithRSAEncryption', 'RSA-SHA384'),
+ '1.2.840.113549.1.1.13': ('sha512WithRSAEncryption', 'RSA-SHA512'),
+ '1.2.840.113549.1.1.14': ('sha224WithRSAEncryption', 'RSA-SHA224'),
+ '1.2.840.113549.1.1.15': ('sha512-224WithRSAEncryption', 'RSA-SHA512/224'),
+ '1.2.840.113549.1.1.16': ('sha512-256WithRSAEncryption', 'RSA-SHA512/256'),
+ '1.2.840.113549.1.3': ('pkcs3', ),
+ '1.2.840.113549.1.3.1': ('dhKeyAgreement', ),
+ '1.2.840.113549.1.5': ('pkcs5', ),
+ '1.2.840.113549.1.5.1': ('pbeWithMD2AndDES-CBC', 'PBE-MD2-DES'),
+ '1.2.840.113549.1.5.3': ('pbeWithMD5AndDES-CBC', 'PBE-MD5-DES'),
+ '1.2.840.113549.1.5.4': ('pbeWithMD2AndRC2-CBC', 'PBE-MD2-RC2-64'),
+ '1.2.840.113549.1.5.6': ('pbeWithMD5AndRC2-CBC', 'PBE-MD5-RC2-64'),
+ '1.2.840.113549.1.5.10': ('pbeWithSHA1AndDES-CBC', 'PBE-SHA1-DES'),
+ '1.2.840.113549.1.5.11': ('pbeWithSHA1AndRC2-CBC', 'PBE-SHA1-RC2-64'),
+ '1.2.840.113549.1.5.12': ('PBKDF2', ),
+ '1.2.840.113549.1.5.13': ('PBES2', ),
+ '1.2.840.113549.1.5.14': ('PBMAC1', ),
+ '1.2.840.113549.1.7': ('pkcs7', ),
+ '1.2.840.113549.1.7.1': ('pkcs7-data', ),
+ '1.2.840.113549.1.7.2': ('pkcs7-signedData', ),
+ '1.2.840.113549.1.7.3': ('pkcs7-envelopedData', ),
+ '1.2.840.113549.1.7.4': ('pkcs7-signedAndEnvelopedData', ),
+ '1.2.840.113549.1.7.5': ('pkcs7-digestData', ),
+ '1.2.840.113549.1.7.6': ('pkcs7-encryptedData', ),
+ '1.2.840.113549.1.9': ('pkcs9', ),
+ '1.2.840.113549.1.9.1': ('emailAddress', ),
+ '1.2.840.113549.1.9.2': ('unstructuredName', ),
+ '1.2.840.113549.1.9.3': ('contentType', ),
+ '1.2.840.113549.1.9.4': ('messageDigest', ),
+ '1.2.840.113549.1.9.5': ('signingTime', ),
+ '1.2.840.113549.1.9.6': ('countersignature', ),
+ '1.2.840.113549.1.9.7': ('challengePassword', ),
+ '1.2.840.113549.1.9.8': ('unstructuredAddress', ),
+ '1.2.840.113549.1.9.9': ('extendedCertificateAttributes', ),
+ '1.2.840.113549.1.9.14': ('Extension Request', 'extReq'),
+ '1.2.840.113549.1.9.15': ('S/MIME Capabilities', 'SMIME-CAPS'),
+ '1.2.840.113549.1.9.16': ('S/MIME', 'SMIME'),
+ '1.2.840.113549.1.9.16.0': ('id-smime-mod', ),
+ '1.2.840.113549.1.9.16.0.1': ('id-smime-mod-cms', ),
+ '1.2.840.113549.1.9.16.0.2': ('id-smime-mod-ess', ),
+ '1.2.840.113549.1.9.16.0.3': ('id-smime-mod-oid', ),
+ '1.2.840.113549.1.9.16.0.4': ('id-smime-mod-msg-v3', ),
+ '1.2.840.113549.1.9.16.0.5': ('id-smime-mod-ets-eSignature-88', ),
+ '1.2.840.113549.1.9.16.0.6': ('id-smime-mod-ets-eSignature-97', ),
+ '1.2.840.113549.1.9.16.0.7': ('id-smime-mod-ets-eSigPolicy-88', ),
+ '1.2.840.113549.1.9.16.0.8': ('id-smime-mod-ets-eSigPolicy-97', ),
+ '1.2.840.113549.1.9.16.1': ('id-smime-ct', ),
+ '1.2.840.113549.1.9.16.1.1': ('id-smime-ct-receipt', ),
+ '1.2.840.113549.1.9.16.1.2': ('id-smime-ct-authData', ),
+ '1.2.840.113549.1.9.16.1.3': ('id-smime-ct-publishCert', ),
+ '1.2.840.113549.1.9.16.1.4': ('id-smime-ct-TSTInfo', ),
+ '1.2.840.113549.1.9.16.1.5': ('id-smime-ct-TDTInfo', ),
+ '1.2.840.113549.1.9.16.1.6': ('id-smime-ct-contentInfo', ),
+ '1.2.840.113549.1.9.16.1.7': ('id-smime-ct-DVCSRequestData', ),
+ '1.2.840.113549.1.9.16.1.8': ('id-smime-ct-DVCSResponseData', ),
+ '1.2.840.113549.1.9.16.1.9': ('id-smime-ct-compressedData', ),
+ '1.2.840.113549.1.9.16.1.19': ('id-smime-ct-contentCollection', ),
+ '1.2.840.113549.1.9.16.1.23': ('id-smime-ct-authEnvelopedData', ),
+ '1.2.840.113549.1.9.16.1.27': ('id-ct-asciiTextWithCRLF', ),
+ '1.2.840.113549.1.9.16.1.28': ('id-ct-xml', ),
+ '1.2.840.113549.1.9.16.2': ('id-smime-aa', ),
+ '1.2.840.113549.1.9.16.2.1': ('id-smime-aa-receiptRequest', ),
+ '1.2.840.113549.1.9.16.2.2': ('id-smime-aa-securityLabel', ),
+ '1.2.840.113549.1.9.16.2.3': ('id-smime-aa-mlExpandHistory', ),
+ '1.2.840.113549.1.9.16.2.4': ('id-smime-aa-contentHint', ),
+ '1.2.840.113549.1.9.16.2.5': ('id-smime-aa-msgSigDigest', ),
+ '1.2.840.113549.1.9.16.2.6': ('id-smime-aa-encapContentType', ),
+ '1.2.840.113549.1.9.16.2.7': ('id-smime-aa-contentIdentifier', ),
+ '1.2.840.113549.1.9.16.2.8': ('id-smime-aa-macValue', ),
+ '1.2.840.113549.1.9.16.2.9': ('id-smime-aa-equivalentLabels', ),
+ '1.2.840.113549.1.9.16.2.10': ('id-smime-aa-contentReference', ),
+ '1.2.840.113549.1.9.16.2.11': ('id-smime-aa-encrypKeyPref', ),
+ '1.2.840.113549.1.9.16.2.12': ('id-smime-aa-signingCertificate', ),
+ '1.2.840.113549.1.9.16.2.13': ('id-smime-aa-smimeEncryptCerts', ),
+ '1.2.840.113549.1.9.16.2.14': ('id-smime-aa-timeStampToken', ),
+ '1.2.840.113549.1.9.16.2.15': ('id-smime-aa-ets-sigPolicyId', ),
+ '1.2.840.113549.1.9.16.2.16': ('id-smime-aa-ets-commitmentType', ),
+ '1.2.840.113549.1.9.16.2.17': ('id-smime-aa-ets-signerLocation', ),
+ '1.2.840.113549.1.9.16.2.18': ('id-smime-aa-ets-signerAttr', ),
+ '1.2.840.113549.1.9.16.2.19': ('id-smime-aa-ets-otherSigCert', ),
+ '1.2.840.113549.1.9.16.2.20': ('id-smime-aa-ets-contentTimestamp', ),
+ '1.2.840.113549.1.9.16.2.21': ('id-smime-aa-ets-CertificateRefs', ),
+ '1.2.840.113549.1.9.16.2.22': ('id-smime-aa-ets-RevocationRefs', ),
+ '1.2.840.113549.1.9.16.2.23': ('id-smime-aa-ets-certValues', ),
+ '1.2.840.113549.1.9.16.2.24': ('id-smime-aa-ets-revocationValues', ),
+ '1.2.840.113549.1.9.16.2.25': ('id-smime-aa-ets-escTimeStamp', ),
+ '1.2.840.113549.1.9.16.2.26': ('id-smime-aa-ets-certCRLTimestamp', ),
+ '1.2.840.113549.1.9.16.2.27': ('id-smime-aa-ets-archiveTimeStamp', ),
+ '1.2.840.113549.1.9.16.2.28': ('id-smime-aa-signatureType', ),
+ '1.2.840.113549.1.9.16.2.29': ('id-smime-aa-dvcs-dvc', ),
+ '1.2.840.113549.1.9.16.2.47': ('id-smime-aa-signingCertificateV2', ),
+ '1.2.840.113549.1.9.16.3': ('id-smime-alg', ),
+ '1.2.840.113549.1.9.16.3.1': ('id-smime-alg-ESDHwith3DES', ),
+ '1.2.840.113549.1.9.16.3.2': ('id-smime-alg-ESDHwithRC2', ),
+ '1.2.840.113549.1.9.16.3.3': ('id-smime-alg-3DESwrap', ),
+ '1.2.840.113549.1.9.16.3.4': ('id-smime-alg-RC2wrap', ),
+ '1.2.840.113549.1.9.16.3.5': ('id-smime-alg-ESDH', ),
+ '1.2.840.113549.1.9.16.3.6': ('id-smime-alg-CMS3DESwrap', ),
+ '1.2.840.113549.1.9.16.3.7': ('id-smime-alg-CMSRC2wrap', ),
+ '1.2.840.113549.1.9.16.3.8': ('zlib compression', 'ZLIB'),
+ '1.2.840.113549.1.9.16.3.9': ('id-alg-PWRI-KEK', ),
+ '1.2.840.113549.1.9.16.4': ('id-smime-cd', ),
+ '1.2.840.113549.1.9.16.4.1': ('id-smime-cd-ldap', ),
+ '1.2.840.113549.1.9.16.5': ('id-smime-spq', ),
+ '1.2.840.113549.1.9.16.5.1': ('id-smime-spq-ets-sqt-uri', ),
+ '1.2.840.113549.1.9.16.5.2': ('id-smime-spq-ets-sqt-unotice', ),
+ '1.2.840.113549.1.9.16.6': ('id-smime-cti', ),
+ '1.2.840.113549.1.9.16.6.1': ('id-smime-cti-ets-proofOfOrigin', ),
+ '1.2.840.113549.1.9.16.6.2': ('id-smime-cti-ets-proofOfReceipt', ),
+ '1.2.840.113549.1.9.16.6.3': ('id-smime-cti-ets-proofOfDelivery', ),
+ '1.2.840.113549.1.9.16.6.4': ('id-smime-cti-ets-proofOfSender', ),
+ '1.2.840.113549.1.9.16.6.5': ('id-smime-cti-ets-proofOfApproval', ),
+ '1.2.840.113549.1.9.16.6.6': ('id-smime-cti-ets-proofOfCreation', ),
+ '1.2.840.113549.1.9.20': ('friendlyName', ),
+ '1.2.840.113549.1.9.21': ('localKeyID', ),
+ '1.2.840.113549.1.9.22': ('certTypes', ),
+ '1.2.840.113549.1.9.22.1': ('x509Certificate', ),
+ '1.2.840.113549.1.9.22.2': ('sdsiCertificate', ),
+ '1.2.840.113549.1.9.23': ('crlTypes', ),
+ '1.2.840.113549.1.9.23.1': ('x509Crl', ),
+ '1.2.840.113549.1.12': ('pkcs12', ),
+ '1.2.840.113549.1.12.1': ('pkcs12-pbeids', ),
+ '1.2.840.113549.1.12.1.1': ('pbeWithSHA1And128BitRC4', 'PBE-SHA1-RC4-128'),
+ '1.2.840.113549.1.12.1.2': ('pbeWithSHA1And40BitRC4', 'PBE-SHA1-RC4-40'),
+ '1.2.840.113549.1.12.1.3': ('pbeWithSHA1And3-KeyTripleDES-CBC', 'PBE-SHA1-3DES'),
+ '1.2.840.113549.1.12.1.4': ('pbeWithSHA1And2-KeyTripleDES-CBC', 'PBE-SHA1-2DES'),
+ '1.2.840.113549.1.12.1.5': ('pbeWithSHA1And128BitRC2-CBC', 'PBE-SHA1-RC2-128'),
+ '1.2.840.113549.1.12.1.6': ('pbeWithSHA1And40BitRC2-CBC', 'PBE-SHA1-RC2-40'),
+ '1.2.840.113549.1.12.10': ('pkcs12-Version1', ),
+ '1.2.840.113549.1.12.10.1': ('pkcs12-BagIds', ),
+ '1.2.840.113549.1.12.10.1.1': ('keyBag', ),
+ '1.2.840.113549.1.12.10.1.2': ('pkcs8ShroudedKeyBag', ),
+ '1.2.840.113549.1.12.10.1.3': ('certBag', ),
+ '1.2.840.113549.1.12.10.1.4': ('crlBag', ),
+ '1.2.840.113549.1.12.10.1.5': ('secretBag', ),
+ '1.2.840.113549.1.12.10.1.6': ('safeContentsBag', ),
+ '1.2.840.113549.2.2': ('md2', 'MD2'),
+ '1.2.840.113549.2.4': ('md4', 'MD4'),
+ '1.2.840.113549.2.5': ('md5', 'MD5'),
+ '1.2.840.113549.2.6': ('hmacWithMD5', ),
+ '1.2.840.113549.2.7': ('hmacWithSHA1', ),
+ '1.2.840.113549.2.8': ('hmacWithSHA224', ),
+ '1.2.840.113549.2.9': ('hmacWithSHA256', ),
+ '1.2.840.113549.2.10': ('hmacWithSHA384', ),
+ '1.2.840.113549.2.11': ('hmacWithSHA512', ),
+ '1.2.840.113549.2.12': ('hmacWithSHA512-224', ),
+ '1.2.840.113549.2.13': ('hmacWithSHA512-256', ),
+ '1.2.840.113549.3.2': ('rc2-cbc', 'RC2-CBC'),
+ '1.2.840.113549.3.4': ('rc4', 'RC4'),
+ '1.2.840.113549.3.7': ('des-ede3-cbc', 'DES-EDE3-CBC'),
+ '1.2.840.113549.3.8': ('rc5-cbc', 'RC5-CBC'),
+ '1.2.840.113549.3.10': ('des-cdmf', 'DES-CDMF'),
+ '1.3': ('identified-organization', 'org', 'ORG'),
+ '1.3.6': ('dod', 'DOD'),
+ '1.3.6.1': ('iana', 'IANA', 'internet'),
+ '1.3.6.1.1': ('Directory', 'directory'),
+ '1.3.6.1.2': ('Management', 'mgmt'),
+ '1.3.6.1.3': ('Experimental', 'experimental'),
+ '1.3.6.1.4': ('Private', 'private'),
+ '1.3.6.1.4.1': ('Enterprises', 'enterprises'),
+ '1.3.6.1.4.1.188.7.1.1.2': ('idea-cbc', 'IDEA-CBC'),
+ '1.3.6.1.4.1.311.2.1.14': ('Microsoft Extension Request', 'msExtReq'),
+ '1.3.6.1.4.1.311.2.1.21': ('Microsoft Individual Code Signing', 'msCodeInd'),
+ '1.3.6.1.4.1.311.2.1.22': ('Microsoft Commercial Code Signing', 'msCodeCom'),
+ '1.3.6.1.4.1.311.10.3.1': ('Microsoft Trust List Signing', 'msCTLSign'),
+ '1.3.6.1.4.1.311.10.3.3': ('Microsoft Server Gated Crypto', 'msSGC'),
+ '1.3.6.1.4.1.311.10.3.4': ('Microsoft Encrypted File System', 'msEFS'),
+ '1.3.6.1.4.1.311.17.1': ('Microsoft CSP Name', 'CSPName'),
+ '1.3.6.1.4.1.311.17.2': ('Microsoft Local Key set', 'LocalKeySet'),
+ '1.3.6.1.4.1.311.20.2.2': ('Microsoft Smartcardlogin', 'msSmartcardLogin'),
+ '1.3.6.1.4.1.311.20.2.3': ('Microsoft Universal Principal Name', 'msUPN'),
+ '1.3.6.1.4.1.311.60.2.1.1': ('jurisdictionLocalityName', 'jurisdictionL'),
+ '1.3.6.1.4.1.311.60.2.1.2': ('jurisdictionStateOrProvinceName', 'jurisdictionST'),
+ '1.3.6.1.4.1.311.60.2.1.3': ('jurisdictionCountryName', 'jurisdictionC'),
+ '1.3.6.1.4.1.1466.344': ('dcObject', 'dcobject'),
+ '1.3.6.1.4.1.1722.12.2.1.16': ('blake2b512', 'BLAKE2b512'),
+ '1.3.6.1.4.1.1722.12.2.2.8': ('blake2s256', 'BLAKE2s256'),
+ '1.3.6.1.4.1.3029.1.2': ('bf-cbc', 'BF-CBC'),
+ '1.3.6.1.4.1.11129.2.4.2': ('CT Precertificate SCTs', 'ct_precert_scts'),
+ '1.3.6.1.4.1.11129.2.4.3': ('CT Precertificate Poison', 'ct_precert_poison'),
+ '1.3.6.1.4.1.11129.2.4.4': ('CT Precertificate Signer', 'ct_precert_signer'),
+ '1.3.6.1.4.1.11129.2.4.5': ('CT Certificate SCTs', 'ct_cert_scts'),
+ '1.3.6.1.4.1.11591.4.11': ('scrypt', 'id-scrypt'),
+ '1.3.6.1.5': ('Security', 'security'),
+ '1.3.6.1.5.2.3': ('id-pkinit', ),
+ '1.3.6.1.5.2.3.4': ('PKINIT Client Auth', 'pkInitClientAuth'),
+ '1.3.6.1.5.2.3.5': ('Signing KDC Response', 'pkInitKDC'),
+ '1.3.6.1.5.5.7': ('PKIX', ),
+ '1.3.6.1.5.5.7.0': ('id-pkix-mod', ),
+ '1.3.6.1.5.5.7.0.1': ('id-pkix1-explicit-88', ),
+ '1.3.6.1.5.5.7.0.2': ('id-pkix1-implicit-88', ),
+ '1.3.6.1.5.5.7.0.3': ('id-pkix1-explicit-93', ),
+ '1.3.6.1.5.5.7.0.4': ('id-pkix1-implicit-93', ),
+ '1.3.6.1.5.5.7.0.5': ('id-mod-crmf', ),
+ '1.3.6.1.5.5.7.0.6': ('id-mod-cmc', ),
+ '1.3.6.1.5.5.7.0.7': ('id-mod-kea-profile-88', ),
+ '1.3.6.1.5.5.7.0.8': ('id-mod-kea-profile-93', ),
+ '1.3.6.1.5.5.7.0.9': ('id-mod-cmp', ),
+ '1.3.6.1.5.5.7.0.10': ('id-mod-qualified-cert-88', ),
+ '1.3.6.1.5.5.7.0.11': ('id-mod-qualified-cert-93', ),
+ '1.3.6.1.5.5.7.0.12': ('id-mod-attribute-cert', ),
+ '1.3.6.1.5.5.7.0.13': ('id-mod-timestamp-protocol', ),
+ '1.3.6.1.5.5.7.0.14': ('id-mod-ocsp', ),
+ '1.3.6.1.5.5.7.0.15': ('id-mod-dvcs', ),
+ '1.3.6.1.5.5.7.0.16': ('id-mod-cmp2000', ),
+ '1.3.6.1.5.5.7.1': ('id-pe', ),
+ '1.3.6.1.5.5.7.1.1': ('Authority Information Access', 'authorityInfoAccess'),
+ '1.3.6.1.5.5.7.1.2': ('Biometric Info', 'biometricInfo'),
+ '1.3.6.1.5.5.7.1.3': ('qcStatements', ),
+ '1.3.6.1.5.5.7.1.4': ('ac-auditEntity', ),
+ '1.3.6.1.5.5.7.1.5': ('ac-targeting', ),
+ '1.3.6.1.5.5.7.1.6': ('aaControls', ),
+ '1.3.6.1.5.5.7.1.7': ('sbgp-ipAddrBlock', ),
+ '1.3.6.1.5.5.7.1.8': ('sbgp-autonomousSysNum', ),
+ '1.3.6.1.5.5.7.1.9': ('sbgp-routerIdentifier', ),
+ '1.3.6.1.5.5.7.1.10': ('ac-proxying', ),
+ '1.3.6.1.5.5.7.1.11': ('Subject Information Access', 'subjectInfoAccess'),
+ '1.3.6.1.5.5.7.1.14': ('Proxy Certificate Information', 'proxyCertInfo'),
+ '1.3.6.1.5.5.7.1.24': ('TLS Feature', 'tlsfeature'),
+ '1.3.6.1.5.5.7.2': ('id-qt', ),
+ '1.3.6.1.5.5.7.2.1': ('Policy Qualifier CPS', 'id-qt-cps'),
+ '1.3.6.1.5.5.7.2.2': ('Policy Qualifier User Notice', 'id-qt-unotice'),
+ '1.3.6.1.5.5.7.2.3': ('textNotice', ),
+ '1.3.6.1.5.5.7.3': ('id-kp', ),
+ '1.3.6.1.5.5.7.3.1': ('TLS Web Server Authentication', 'serverAuth'),
+ '1.3.6.1.5.5.7.3.2': ('TLS Web Client Authentication', 'clientAuth'),
+ '1.3.6.1.5.5.7.3.3': ('Code Signing', 'codeSigning'),
+ '1.3.6.1.5.5.7.3.4': ('E-mail Protection', 'emailProtection'),
+ '1.3.6.1.5.5.7.3.5': ('IPSec End System', 'ipsecEndSystem'),
+ '1.3.6.1.5.5.7.3.6': ('IPSec Tunnel', 'ipsecTunnel'),
+ '1.3.6.1.5.5.7.3.7': ('IPSec User', 'ipsecUser'),
+ '1.3.6.1.5.5.7.3.8': ('Time Stamping', 'timeStamping'),
+ '1.3.6.1.5.5.7.3.9': ('OCSP Signing', 'OCSPSigning'),
+ '1.3.6.1.5.5.7.3.10': ('dvcs', 'DVCS'),
+ '1.3.6.1.5.5.7.3.17': ('ipsec Internet Key Exchange', 'ipsecIKE'),
+ '1.3.6.1.5.5.7.3.18': ('Ctrl/provision WAP Access', 'capwapAC'),
+ '1.3.6.1.5.5.7.3.19': ('Ctrl/Provision WAP Termination', 'capwapWTP'),
+ '1.3.6.1.5.5.7.3.21': ('SSH Client', 'secureShellClient'),
+ '1.3.6.1.5.5.7.3.22': ('SSH Server', 'secureShellServer'),
+ '1.3.6.1.5.5.7.3.23': ('Send Router', 'sendRouter'),
+ '1.3.6.1.5.5.7.3.24': ('Send Proxied Router', 'sendProxiedRouter'),
+ '1.3.6.1.5.5.7.3.25': ('Send Owner', 'sendOwner'),
+ '1.3.6.1.5.5.7.3.26': ('Send Proxied Owner', 'sendProxiedOwner'),
+ '1.3.6.1.5.5.7.3.27': ('CMC Certificate Authority', 'cmcCA'),
+ '1.3.6.1.5.5.7.3.28': ('CMC Registration Authority', 'cmcRA'),
+ '1.3.6.1.5.5.7.4': ('id-it', ),
+ '1.3.6.1.5.5.7.4.1': ('id-it-caProtEncCert', ),
+ '1.3.6.1.5.5.7.4.2': ('id-it-signKeyPairTypes', ),
+ '1.3.6.1.5.5.7.4.3': ('id-it-encKeyPairTypes', ),
+ '1.3.6.1.5.5.7.4.4': ('id-it-preferredSymmAlg', ),
+ '1.3.6.1.5.5.7.4.5': ('id-it-caKeyUpdateInfo', ),
+ '1.3.6.1.5.5.7.4.6': ('id-it-currentCRL', ),
+ '1.3.6.1.5.5.7.4.7': ('id-it-unsupportedOIDs', ),
+ '1.3.6.1.5.5.7.4.8': ('id-it-subscriptionRequest', ),
+ '1.3.6.1.5.5.7.4.9': ('id-it-subscriptionResponse', ),
+ '1.3.6.1.5.5.7.4.10': ('id-it-keyPairParamReq', ),
+ '1.3.6.1.5.5.7.4.11': ('id-it-keyPairParamRep', ),
+ '1.3.6.1.5.5.7.4.12': ('id-it-revPassphrase', ),
+ '1.3.6.1.5.5.7.4.13': ('id-it-implicitConfirm', ),
+ '1.3.6.1.5.5.7.4.14': ('id-it-confirmWaitTime', ),
+ '1.3.6.1.5.5.7.4.15': ('id-it-origPKIMessage', ),
+ '1.3.6.1.5.5.7.4.16': ('id-it-suppLangTags', ),
+ '1.3.6.1.5.5.7.5': ('id-pkip', ),
+ '1.3.6.1.5.5.7.5.1': ('id-regCtrl', ),
+ '1.3.6.1.5.5.7.5.1.1': ('id-regCtrl-regToken', ),
+ '1.3.6.1.5.5.7.5.1.2': ('id-regCtrl-authenticator', ),
+ '1.3.6.1.5.5.7.5.1.3': ('id-regCtrl-pkiPublicationInfo', ),
+ '1.3.6.1.5.5.7.5.1.4': ('id-regCtrl-pkiArchiveOptions', ),
+ '1.3.6.1.5.5.7.5.1.5': ('id-regCtrl-oldCertID', ),
+ '1.3.6.1.5.5.7.5.1.6': ('id-regCtrl-protocolEncrKey', ),
+ '1.3.6.1.5.5.7.5.2': ('id-regInfo', ),
+ '1.3.6.1.5.5.7.5.2.1': ('id-regInfo-utf8Pairs', ),
+ '1.3.6.1.5.5.7.5.2.2': ('id-regInfo-certReq', ),
+ '1.3.6.1.5.5.7.6': ('id-alg', ),
+ '1.3.6.1.5.5.7.6.1': ('id-alg-des40', ),
+ '1.3.6.1.5.5.7.6.2': ('id-alg-noSignature', ),
+ '1.3.6.1.5.5.7.6.3': ('id-alg-dh-sig-hmac-sha1', ),
+ '1.3.6.1.5.5.7.6.4': ('id-alg-dh-pop', ),
+ '1.3.6.1.5.5.7.7': ('id-cmc', ),
+ '1.3.6.1.5.5.7.7.1': ('id-cmc-statusInfo', ),
+ '1.3.6.1.5.5.7.7.2': ('id-cmc-identification', ),
+ '1.3.6.1.5.5.7.7.3': ('id-cmc-identityProof', ),
+ '1.3.6.1.5.5.7.7.4': ('id-cmc-dataReturn', ),
+ '1.3.6.1.5.5.7.7.5': ('id-cmc-transactionId', ),
+ '1.3.6.1.5.5.7.7.6': ('id-cmc-senderNonce', ),
+ '1.3.6.1.5.5.7.7.7': ('id-cmc-recipientNonce', ),
+ '1.3.6.1.5.5.7.7.8': ('id-cmc-addExtensions', ),
+ '1.3.6.1.5.5.7.7.9': ('id-cmc-encryptedPOP', ),
+ '1.3.6.1.5.5.7.7.10': ('id-cmc-decryptedPOP', ),
+ '1.3.6.1.5.5.7.7.11': ('id-cmc-lraPOPWitness', ),
+ '1.3.6.1.5.5.7.7.15': ('id-cmc-getCert', ),
+ '1.3.6.1.5.5.7.7.16': ('id-cmc-getCRL', ),
+ '1.3.6.1.5.5.7.7.17': ('id-cmc-revokeRequest', ),
+ '1.3.6.1.5.5.7.7.18': ('id-cmc-regInfo', ),
+ '1.3.6.1.5.5.7.7.19': ('id-cmc-responseInfo', ),
+ '1.3.6.1.5.5.7.7.21': ('id-cmc-queryPending', ),
+ '1.3.6.1.5.5.7.7.22': ('id-cmc-popLinkRandom', ),
+ '1.3.6.1.5.5.7.7.23': ('id-cmc-popLinkWitness', ),
+ '1.3.6.1.5.5.7.7.24': ('id-cmc-confirmCertAcceptance', ),
+ '1.3.6.1.5.5.7.8': ('id-on', ),
+ '1.3.6.1.5.5.7.8.1': ('id-on-personalData', ),
+ '1.3.6.1.5.5.7.8.3': ('Permanent Identifier', 'id-on-permanentIdentifier'),
+ '1.3.6.1.5.5.7.9': ('id-pda', ),
+ '1.3.6.1.5.5.7.9.1': ('id-pda-dateOfBirth', ),
+ '1.3.6.1.5.5.7.9.2': ('id-pda-placeOfBirth', ),
+ '1.3.6.1.5.5.7.9.3': ('id-pda-gender', ),
+ '1.3.6.1.5.5.7.9.4': ('id-pda-countryOfCitizenship', ),
+ '1.3.6.1.5.5.7.9.5': ('id-pda-countryOfResidence', ),
+ '1.3.6.1.5.5.7.10': ('id-aca', ),
+ '1.3.6.1.5.5.7.10.1': ('id-aca-authenticationInfo', ),
+ '1.3.6.1.5.5.7.10.2': ('id-aca-accessIdentity', ),
+ '1.3.6.1.5.5.7.10.3': ('id-aca-chargingIdentity', ),
+ '1.3.6.1.5.5.7.10.4': ('id-aca-group', ),
+ '1.3.6.1.5.5.7.10.5': ('id-aca-role', ),
+ '1.3.6.1.5.5.7.10.6': ('id-aca-encAttrs', ),
+ '1.3.6.1.5.5.7.11': ('id-qcs', ),
+ '1.3.6.1.5.5.7.11.1': ('id-qcs-pkixQCSyntax-v1', ),
+ '1.3.6.1.5.5.7.12': ('id-cct', ),
+ '1.3.6.1.5.5.7.12.1': ('id-cct-crs', ),
+ '1.3.6.1.5.5.7.12.2': ('id-cct-PKIData', ),
+ '1.3.6.1.5.5.7.12.3': ('id-cct-PKIResponse', ),
+ '1.3.6.1.5.5.7.21': ('id-ppl', ),
+ '1.3.6.1.5.5.7.21.0': ('Any language', 'id-ppl-anyLanguage'),
+ '1.3.6.1.5.5.7.21.1': ('Inherit all', 'id-ppl-inheritAll'),
+ '1.3.6.1.5.5.7.21.2': ('Independent', 'id-ppl-independent'),
+ '1.3.6.1.5.5.7.48': ('id-ad', ),
+ '1.3.6.1.5.5.7.48.1': ('OCSP', 'OCSP', 'id-pkix-OCSP'),
+ '1.3.6.1.5.5.7.48.1.1': ('Basic OCSP Response', 'basicOCSPResponse'),
+ '1.3.6.1.5.5.7.48.1.2': ('OCSP Nonce', 'Nonce'),
+ '1.3.6.1.5.5.7.48.1.3': ('OCSP CRL ID', 'CrlID'),
+ '1.3.6.1.5.5.7.48.1.4': ('Acceptable OCSP Responses', 'acceptableResponses'),
+ '1.3.6.1.5.5.7.48.1.5': ('OCSP No Check', 'noCheck'),
+ '1.3.6.1.5.5.7.48.1.6': ('OCSP Archive Cutoff', 'archiveCutoff'),
+ '1.3.6.1.5.5.7.48.1.7': ('OCSP Service Locator', 'serviceLocator'),
+ '1.3.6.1.5.5.7.48.1.8': ('Extended OCSP Status', 'extendedStatus'),
+ '1.3.6.1.5.5.7.48.1.9': ('valid', ),
+ '1.3.6.1.5.5.7.48.1.10': ('path', ),
+ '1.3.6.1.5.5.7.48.1.11': ('Trust Root', 'trustRoot'),
+ '1.3.6.1.5.5.7.48.2': ('CA Issuers', 'caIssuers'),
+ '1.3.6.1.5.5.7.48.3': ('AD Time Stamping', 'ad_timestamping'),
+ '1.3.6.1.5.5.7.48.4': ('ad dvcs', 'AD_DVCS'),
+ '1.3.6.1.5.5.7.48.5': ('CA Repository', 'caRepository'),
+ '1.3.6.1.5.5.8.1.1': ('hmac-md5', 'HMAC-MD5'),
+ '1.3.6.1.5.5.8.1.2': ('hmac-sha1', 'HMAC-SHA1'),
+ '1.3.6.1.6': ('SNMPv2', 'snmpv2'),
+ '1.3.6.1.7': ('Mail', ),
+ '1.3.6.1.7.1': ('MIME MHS', 'mime-mhs'),
+ '1.3.6.1.7.1.1': ('mime-mhs-headings', 'mime-mhs-headings'),
+ '1.3.6.1.7.1.1.1': ('id-hex-partial-message', 'id-hex-partial-message'),
+ '1.3.6.1.7.1.1.2': ('id-hex-multipart-message', 'id-hex-multipart-message'),
+ '1.3.6.1.7.1.2': ('mime-mhs-bodies', 'mime-mhs-bodies'),
+ '1.3.14.3.2': ('algorithm', 'algorithm'),
+ '1.3.14.3.2.3': ('md5WithRSA', 'RSA-NP-MD5'),
+ '1.3.14.3.2.6': ('des-ecb', 'DES-ECB'),
+ '1.3.14.3.2.7': ('des-cbc', 'DES-CBC'),
+ '1.3.14.3.2.8': ('des-ofb', 'DES-OFB'),
+ '1.3.14.3.2.9': ('des-cfb', 'DES-CFB'),
+ '1.3.14.3.2.11': ('rsaSignature', ),
+ '1.3.14.3.2.12': ('dsaEncryption-old', 'DSA-old'),
+ '1.3.14.3.2.13': ('dsaWithSHA', 'DSA-SHA'),
+ '1.3.14.3.2.15': ('shaWithRSAEncryption', 'RSA-SHA'),
+ '1.3.14.3.2.17': ('des-ede', 'DES-EDE'),
+ '1.3.14.3.2.18': ('sha', 'SHA'),
+ '1.3.14.3.2.26': ('sha1', 'SHA1'),
+ '1.3.14.3.2.27': ('dsaWithSHA1-old', 'DSA-SHA1-old'),
+ '1.3.14.3.2.29': ('sha1WithRSA', 'RSA-SHA1-2'),
+ '1.3.36.3.2.1': ('ripemd160', 'RIPEMD160'),
+ '1.3.36.3.3.1.2': ('ripemd160WithRSA', 'RSA-RIPEMD160'),
+ '1.3.36.3.3.2.8.1.1.1': ('brainpoolP160r1', ),
+ '1.3.36.3.3.2.8.1.1.2': ('brainpoolP160t1', ),
+ '1.3.36.3.3.2.8.1.1.3': ('brainpoolP192r1', ),
+ '1.3.36.3.3.2.8.1.1.4': ('brainpoolP192t1', ),
+ '1.3.36.3.3.2.8.1.1.5': ('brainpoolP224r1', ),
+ '1.3.36.3.3.2.8.1.1.6': ('brainpoolP224t1', ),
+ '1.3.36.3.3.2.8.1.1.7': ('brainpoolP256r1', ),
+ '1.3.36.3.3.2.8.1.1.8': ('brainpoolP256t1', ),
+ '1.3.36.3.3.2.8.1.1.9': ('brainpoolP320r1', ),
+ '1.3.36.3.3.2.8.1.1.10': ('brainpoolP320t1', ),
+ '1.3.36.3.3.2.8.1.1.11': ('brainpoolP384r1', ),
+ '1.3.36.3.3.2.8.1.1.12': ('brainpoolP384t1', ),
+ '1.3.36.3.3.2.8.1.1.13': ('brainpoolP512r1', ),
+ '1.3.36.3.3.2.8.1.1.14': ('brainpoolP512t1', ),
+ '1.3.36.8.3.3': ('Professional Information or basis for Admission', 'x509ExtAdmission'),
+ '1.3.101.1.4.1': ('Strong Extranet ID', 'SXNetID'),
+ '1.3.101.110': ('X25519', ),
+ '1.3.101.111': ('X448', ),
+ '1.3.101.112': ('ED25519', ),
+ '1.3.101.113': ('ED448', ),
+ '1.3.111': ('ieee', ),
+ '1.3.111.2.1619': ('IEEE Security in Storage Working Group', 'ieee-siswg'),
+ '1.3.111.2.1619.0.1.1': ('aes-128-xts', 'AES-128-XTS'),
+ '1.3.111.2.1619.0.1.2': ('aes-256-xts', 'AES-256-XTS'),
+ '1.3.132': ('certicom-arc', ),
+ '1.3.132.0': ('secg_ellipticCurve', ),
+ '1.3.132.0.1': ('sect163k1', ),
+ '1.3.132.0.2': ('sect163r1', ),
+ '1.3.132.0.3': ('sect239k1', ),
+ '1.3.132.0.4': ('sect113r1', ),
+ '1.3.132.0.5': ('sect113r2', ),
+ '1.3.132.0.6': ('secp112r1', ),
+ '1.3.132.0.7': ('secp112r2', ),
+ '1.3.132.0.8': ('secp160r1', ),
+ '1.3.132.0.9': ('secp160k1', ),
+ '1.3.132.0.10': ('secp256k1', ),
+ '1.3.132.0.15': ('sect163r2', ),
+ '1.3.132.0.16': ('sect283k1', ),
+ '1.3.132.0.17': ('sect283r1', ),
+ '1.3.132.0.22': ('sect131r1', ),
+ '1.3.132.0.23': ('sect131r2', ),
+ '1.3.132.0.24': ('sect193r1', ),
+ '1.3.132.0.25': ('sect193r2', ),
+ '1.3.132.0.26': ('sect233k1', ),
+ '1.3.132.0.27': ('sect233r1', ),
+ '1.3.132.0.28': ('secp128r1', ),
+ '1.3.132.0.29': ('secp128r2', ),
+ '1.3.132.0.30': ('secp160r2', ),
+ '1.3.132.0.31': ('secp192k1', ),
+ '1.3.132.0.32': ('secp224k1', ),
+ '1.3.132.0.33': ('secp224r1', ),
+ '1.3.132.0.34': ('secp384r1', ),
+ '1.3.132.0.35': ('secp521r1', ),
+ '1.3.132.0.36': ('sect409k1', ),
+ '1.3.132.0.37': ('sect409r1', ),
+ '1.3.132.0.38': ('sect571k1', ),
+ '1.3.132.0.39': ('sect571r1', ),
+ '1.3.132.1': ('secg-scheme', ),
+ '1.3.132.1.11.0': ('dhSinglePass-stdDH-sha224kdf-scheme', ),
+ '1.3.132.1.11.1': ('dhSinglePass-stdDH-sha256kdf-scheme', ),
+ '1.3.132.1.11.2': ('dhSinglePass-stdDH-sha384kdf-scheme', ),
+ '1.3.132.1.11.3': ('dhSinglePass-stdDH-sha512kdf-scheme', ),
+ '1.3.132.1.14.0': ('dhSinglePass-cofactorDH-sha224kdf-scheme', ),
+ '1.3.132.1.14.1': ('dhSinglePass-cofactorDH-sha256kdf-scheme', ),
+ '1.3.132.1.14.2': ('dhSinglePass-cofactorDH-sha384kdf-scheme', ),
+ '1.3.132.1.14.3': ('dhSinglePass-cofactorDH-sha512kdf-scheme', ),
+ '1.3.133.16.840.63.0': ('x9-63-scheme', ),
+ '1.3.133.16.840.63.0.2': ('dhSinglePass-stdDH-sha1kdf-scheme', ),
+ '1.3.133.16.840.63.0.3': ('dhSinglePass-cofactorDH-sha1kdf-scheme', ),
+ '2': ('joint-iso-itu-t', 'JOINT-ISO-ITU-T', 'joint-iso-ccitt'),
+ '2.5': ('directory services (X.500)', 'X500'),
+ '2.5.1.5': ('Selected Attribute Types', 'selected-attribute-types'),
+ '2.5.1.5.55': ('clearance', ),
+ '2.5.4': ('X509', ),
+ '2.5.4.3': ('commonName', 'CN'),
+ '2.5.4.4': ('surname', 'SN'),
+ '2.5.4.5': ('serialNumber', ),
+ '2.5.4.6': ('countryName', 'C'),
+ '2.5.4.7': ('localityName', 'L'),
+ '2.5.4.8': ('stateOrProvinceName', 'ST'),
+ '2.5.4.9': ('streetAddress', 'street'),
+ '2.5.4.10': ('organizationName', 'O'),
+ '2.5.4.11': ('organizationalUnitName', 'OU'),
+ '2.5.4.12': ('title', 'title'),
+ '2.5.4.13': ('description', ),
+ '2.5.4.14': ('searchGuide', ),
+ '2.5.4.15': ('businessCategory', ),
+ '2.5.4.16': ('postalAddress', ),
+ '2.5.4.17': ('postalCode', ),
+ '2.5.4.18': ('postOfficeBox', ),
+ '2.5.4.19': ('physicalDeliveryOfficeName', ),
+ '2.5.4.20': ('telephoneNumber', ),
+ '2.5.4.21': ('telexNumber', ),
+ '2.5.4.22': ('teletexTerminalIdentifier', ),
+ '2.5.4.23': ('facsimileTelephoneNumber', ),
+ '2.5.4.24': ('x121Address', ),
+ '2.5.4.25': ('internationaliSDNNumber', ),
+ '2.5.4.26': ('registeredAddress', ),
+ '2.5.4.27': ('destinationIndicator', ),
+ '2.5.4.28': ('preferredDeliveryMethod', ),
+ '2.5.4.29': ('presentationAddress', ),
+ '2.5.4.30': ('supportedApplicationContext', ),
+ '2.5.4.31': ('member', ),
+ '2.5.4.32': ('owner', ),
+ '2.5.4.33': ('roleOccupant', ),
+ '2.5.4.34': ('seeAlso', ),
+ '2.5.4.35': ('userPassword', ),
+ '2.5.4.36': ('userCertificate', ),
+ '2.5.4.37': ('cACertificate', ),
+ '2.5.4.38': ('authorityRevocationList', ),
+ '2.5.4.39': ('certificateRevocationList', ),
+ '2.5.4.40': ('crossCertificatePair', ),
+ '2.5.4.41': ('name', 'name'),
+ '2.5.4.42': ('givenName', 'GN'),
+ '2.5.4.43': ('initials', 'initials'),
+ '2.5.4.44': ('generationQualifier', ),
+ '2.5.4.45': ('x500UniqueIdentifier', ),
+ '2.5.4.46': ('dnQualifier', 'dnQualifier'),
+ '2.5.4.47': ('enhancedSearchGuide', ),
+ '2.5.4.48': ('protocolInformation', ),
+ '2.5.4.49': ('distinguishedName', ),
+ '2.5.4.50': ('uniqueMember', ),
+ '2.5.4.51': ('houseIdentifier', ),
+ '2.5.4.52': ('supportedAlgorithms', ),
+ '2.5.4.53': ('deltaRevocationList', ),
+ '2.5.4.54': ('dmdName', ),
+ '2.5.4.65': ('pseudonym', ),
+ '2.5.4.72': ('role', 'role'),
+ '2.5.4.97': ('organizationIdentifier', ),
+ '2.5.4.98': ('countryCode3c', 'c3'),
+ '2.5.4.99': ('countryCode3n', 'n3'),
+ '2.5.4.100': ('dnsName', ),
+ '2.5.8': ('directory services - algorithms', 'X500algorithms'),
+ '2.5.8.1.1': ('rsa', 'RSA'),
+ '2.5.8.3.100': ('mdc2WithRSA', 'RSA-MDC2'),
+ '2.5.8.3.101': ('mdc2', 'MDC2'),
+ '2.5.29': ('id-ce', ),
+ '2.5.29.9': ('X509v3 Subject Directory Attributes', 'subjectDirectoryAttributes'),
+ '2.5.29.14': ('X509v3 Subject Key Identifier', 'subjectKeyIdentifier'),
+ '2.5.29.15': ('X509v3 Key Usage', 'keyUsage'),
+ '2.5.29.16': ('X509v3 Private Key Usage Period', 'privateKeyUsagePeriod'),
+ '2.5.29.17': ('X509v3 Subject Alternative Name', 'subjectAltName'),
+ '2.5.29.18': ('X509v3 Issuer Alternative Name', 'issuerAltName'),
+ '2.5.29.19': ('X509v3 Basic Constraints', 'basicConstraints'),
+ '2.5.29.20': ('X509v3 CRL Number', 'crlNumber'),
+ '2.5.29.21': ('X509v3 CRL Reason Code', 'CRLReason'),
+ '2.5.29.23': ('Hold Instruction Code', 'holdInstructionCode'),
+ '2.5.29.24': ('Invalidity Date', 'invalidityDate'),
+ '2.5.29.27': ('X509v3 Delta CRL Indicator', 'deltaCRL'),
+ '2.5.29.28': ('X509v3 Issuing Distribution Point', 'issuingDistributionPoint'),
+ '2.5.29.29': ('X509v3 Certificate Issuer', 'certificateIssuer'),
+ '2.5.29.30': ('X509v3 Name Constraints', 'nameConstraints'),
+ '2.5.29.31': ('X509v3 CRL Distribution Points', 'crlDistributionPoints'),
+ '2.5.29.32': ('X509v3 Certificate Policies', 'certificatePolicies'),
+ '2.5.29.32.0': ('X509v3 Any Policy', 'anyPolicy'),
+ '2.5.29.33': ('X509v3 Policy Mappings', 'policyMappings'),
+ '2.5.29.35': ('X509v3 Authority Key Identifier', 'authorityKeyIdentifier'),
+ '2.5.29.36': ('X509v3 Policy Constraints', 'policyConstraints'),
+ '2.5.29.37': ('X509v3 Extended Key Usage', 'extendedKeyUsage'),
+ '2.5.29.37.0': ('Any Extended Key Usage', 'anyExtendedKeyUsage'),
+ '2.5.29.46': ('X509v3 Freshest CRL', 'freshestCRL'),
+ '2.5.29.54': ('X509v3 Inhibit Any Policy', 'inhibitAnyPolicy'),
+ '2.5.29.55': ('X509v3 AC Targeting', 'targetInformation'),
+ '2.5.29.56': ('X509v3 No Revocation Available', 'noRevAvail'),
+ '2.16.840.1.101.3': ('csor', ),
+ '2.16.840.1.101.3.4': ('nistAlgorithms', ),
+ '2.16.840.1.101.3.4.1': ('aes', ),
+ '2.16.840.1.101.3.4.1.1': ('aes-128-ecb', 'AES-128-ECB'),
+ '2.16.840.1.101.3.4.1.2': ('aes-128-cbc', 'AES-128-CBC'),
+ '2.16.840.1.101.3.4.1.3': ('aes-128-ofb', 'AES-128-OFB'),
+ '2.16.840.1.101.3.4.1.4': ('aes-128-cfb', 'AES-128-CFB'),
+ '2.16.840.1.101.3.4.1.5': ('id-aes128-wrap', ),
+ '2.16.840.1.101.3.4.1.6': ('aes-128-gcm', 'id-aes128-GCM'),
+ '2.16.840.1.101.3.4.1.7': ('aes-128-ccm', 'id-aes128-CCM'),
+ '2.16.840.1.101.3.4.1.8': ('id-aes128-wrap-pad', ),
+ '2.16.840.1.101.3.4.1.21': ('aes-192-ecb', 'AES-192-ECB'),
+ '2.16.840.1.101.3.4.1.22': ('aes-192-cbc', 'AES-192-CBC'),
+ '2.16.840.1.101.3.4.1.23': ('aes-192-ofb', 'AES-192-OFB'),
+ '2.16.840.1.101.3.4.1.24': ('aes-192-cfb', 'AES-192-CFB'),
+ '2.16.840.1.101.3.4.1.25': ('id-aes192-wrap', ),
+ '2.16.840.1.101.3.4.1.26': ('aes-192-gcm', 'id-aes192-GCM'),
+ '2.16.840.1.101.3.4.1.27': ('aes-192-ccm', 'id-aes192-CCM'),
+ '2.16.840.1.101.3.4.1.28': ('id-aes192-wrap-pad', ),
+ '2.16.840.1.101.3.4.1.41': ('aes-256-ecb', 'AES-256-ECB'),
+ '2.16.840.1.101.3.4.1.42': ('aes-256-cbc', 'AES-256-CBC'),
+ '2.16.840.1.101.3.4.1.43': ('aes-256-ofb', 'AES-256-OFB'),
+ '2.16.840.1.101.3.4.1.44': ('aes-256-cfb', 'AES-256-CFB'),
+ '2.16.840.1.101.3.4.1.45': ('id-aes256-wrap', ),
+ '2.16.840.1.101.3.4.1.46': ('aes-256-gcm', 'id-aes256-GCM'),
+ '2.16.840.1.101.3.4.1.47': ('aes-256-ccm', 'id-aes256-CCM'),
+ '2.16.840.1.101.3.4.1.48': ('id-aes256-wrap-pad', ),
+ '2.16.840.1.101.3.4.2': ('nist_hashalgs', ),
+ '2.16.840.1.101.3.4.2.1': ('sha256', 'SHA256'),
+ '2.16.840.1.101.3.4.2.2': ('sha384', 'SHA384'),
+ '2.16.840.1.101.3.4.2.3': ('sha512', 'SHA512'),
+ '2.16.840.1.101.3.4.2.4': ('sha224', 'SHA224'),
+ '2.16.840.1.101.3.4.2.5': ('sha512-224', 'SHA512-224'),
+ '2.16.840.1.101.3.4.2.6': ('sha512-256', 'SHA512-256'),
+ '2.16.840.1.101.3.4.2.7': ('sha3-224', 'SHA3-224'),
+ '2.16.840.1.101.3.4.2.8': ('sha3-256', 'SHA3-256'),
+ '2.16.840.1.101.3.4.2.9': ('sha3-384', 'SHA3-384'),
+ '2.16.840.1.101.3.4.2.10': ('sha3-512', 'SHA3-512'),
+ '2.16.840.1.101.3.4.2.11': ('shake128', 'SHAKE128'),
+ '2.16.840.1.101.3.4.2.12': ('shake256', 'SHAKE256'),
+ '2.16.840.1.101.3.4.2.13': ('hmac-sha3-224', 'id-hmacWithSHA3-224'),
+ '2.16.840.1.101.3.4.2.14': ('hmac-sha3-256', 'id-hmacWithSHA3-256'),
+ '2.16.840.1.101.3.4.2.15': ('hmac-sha3-384', 'id-hmacWithSHA3-384'),
+ '2.16.840.1.101.3.4.2.16': ('hmac-sha3-512', 'id-hmacWithSHA3-512'),
+ '2.16.840.1.101.3.4.3': ('dsa_with_sha2', 'sigAlgs'),
+ '2.16.840.1.101.3.4.3.1': ('dsa_with_SHA224', ),
+ '2.16.840.1.101.3.4.3.2': ('dsa_with_SHA256', ),
+ '2.16.840.1.101.3.4.3.3': ('dsa_with_SHA384', 'id-dsa-with-sha384'),
+ '2.16.840.1.101.3.4.3.4': ('dsa_with_SHA512', 'id-dsa-with-sha512'),
+ '2.16.840.1.101.3.4.3.5': ('dsa_with_SHA3-224', 'id-dsa-with-sha3-224'),
+ '2.16.840.1.101.3.4.3.6': ('dsa_with_SHA3-256', 'id-dsa-with-sha3-256'),
+ '2.16.840.1.101.3.4.3.7': ('dsa_with_SHA3-384', 'id-dsa-with-sha3-384'),
+ '2.16.840.1.101.3.4.3.8': ('dsa_with_SHA3-512', 'id-dsa-with-sha3-512'),
+ '2.16.840.1.101.3.4.3.9': ('ecdsa_with_SHA3-224', 'id-ecdsa-with-sha3-224'),
+ '2.16.840.1.101.3.4.3.10': ('ecdsa_with_SHA3-256', 'id-ecdsa-with-sha3-256'),
+ '2.16.840.1.101.3.4.3.11': ('ecdsa_with_SHA3-384', 'id-ecdsa-with-sha3-384'),
+ '2.16.840.1.101.3.4.3.12': ('ecdsa_with_SHA3-512', 'id-ecdsa-with-sha3-512'),
+ '2.16.840.1.101.3.4.3.13': ('RSA-SHA3-224', 'id-rsassa-pkcs1-v1_5-with-sha3-224'),
+ '2.16.840.1.101.3.4.3.14': ('RSA-SHA3-256', 'id-rsassa-pkcs1-v1_5-with-sha3-256'),
+ '2.16.840.1.101.3.4.3.15': ('RSA-SHA3-384', 'id-rsassa-pkcs1-v1_5-with-sha3-384'),
+ '2.16.840.1.101.3.4.3.16': ('RSA-SHA3-512', 'id-rsassa-pkcs1-v1_5-with-sha3-512'),
+ '2.16.840.1.113730': ('Netscape Communications Corp.', 'Netscape'),
+ '2.16.840.1.113730.1': ('Netscape Certificate Extension', 'nsCertExt'),
+ '2.16.840.1.113730.1.1': ('Netscape Cert Type', 'nsCertType'),
+ '2.16.840.1.113730.1.2': ('Netscape Base Url', 'nsBaseUrl'),
+ '2.16.840.1.113730.1.3': ('Netscape Revocation Url', 'nsRevocationUrl'),
+ '2.16.840.1.113730.1.4': ('Netscape CA Revocation Url', 'nsCaRevocationUrl'),
+ '2.16.840.1.113730.1.7': ('Netscape Renewal Url', 'nsRenewalUrl'),
+ '2.16.840.1.113730.1.8': ('Netscape CA Policy Url', 'nsCaPolicyUrl'),
+ '2.16.840.1.113730.1.12': ('Netscape SSL Server Name', 'nsSslServerName'),
+ '2.16.840.1.113730.1.13': ('Netscape Comment', 'nsComment'),
+ '2.16.840.1.113730.2': ('Netscape Data Type', 'nsDataType'),
+ '2.16.840.1.113730.2.5': ('Netscape Certificate Sequence', 'nsCertSequence'),
+ '2.16.840.1.113730.4.1': ('Netscape Server Gated Crypto', 'nsSGC'),
+ '2.23': ('International Organizations', 'international-organizations'),
+ '2.23.42': ('Secure Electronic Transactions', 'id-set'),
+ '2.23.42.0': ('content types', 'set-ctype'),
+ '2.23.42.0.0': ('setct-PANData', ),
+ '2.23.42.0.1': ('setct-PANToken', ),
+ '2.23.42.0.2': ('setct-PANOnly', ),
+ '2.23.42.0.3': ('setct-OIData', ),
+ '2.23.42.0.4': ('setct-PI', ),
+ '2.23.42.0.5': ('setct-PIData', ),
+ '2.23.42.0.6': ('setct-PIDataUnsigned', ),
+ '2.23.42.0.7': ('setct-HODInput', ),
+ '2.23.42.0.8': ('setct-AuthResBaggage', ),
+ '2.23.42.0.9': ('setct-AuthRevReqBaggage', ),
+ '2.23.42.0.10': ('setct-AuthRevResBaggage', ),
+ '2.23.42.0.11': ('setct-CapTokenSeq', ),
+ '2.23.42.0.12': ('setct-PInitResData', ),
+ '2.23.42.0.13': ('setct-PI-TBS', ),
+ '2.23.42.0.14': ('setct-PResData', ),
+ '2.23.42.0.16': ('setct-AuthReqTBS', ),
+ '2.23.42.0.17': ('setct-AuthResTBS', ),
+ '2.23.42.0.18': ('setct-AuthResTBSX', ),
+ '2.23.42.0.19': ('setct-AuthTokenTBS', ),
+ '2.23.42.0.20': ('setct-CapTokenData', ),
+ '2.23.42.0.21': ('setct-CapTokenTBS', ),
+ '2.23.42.0.22': ('setct-AcqCardCodeMsg', ),
+ '2.23.42.0.23': ('setct-AuthRevReqTBS', ),
+ '2.23.42.0.24': ('setct-AuthRevResData', ),
+ '2.23.42.0.25': ('setct-AuthRevResTBS', ),
+ '2.23.42.0.26': ('setct-CapReqTBS', ),
+ '2.23.42.0.27': ('setct-CapReqTBSX', ),
+ '2.23.42.0.28': ('setct-CapResData', ),
+ '2.23.42.0.29': ('setct-CapRevReqTBS', ),
+ '2.23.42.0.30': ('setct-CapRevReqTBSX', ),
+ '2.23.42.0.31': ('setct-CapRevResData', ),
+ '2.23.42.0.32': ('setct-CredReqTBS', ),
+ '2.23.42.0.33': ('setct-CredReqTBSX', ),
+ '2.23.42.0.34': ('setct-CredResData', ),
+ '2.23.42.0.35': ('setct-CredRevReqTBS', ),
+ '2.23.42.0.36': ('setct-CredRevReqTBSX', ),
+ '2.23.42.0.37': ('setct-CredRevResData', ),
+ '2.23.42.0.38': ('setct-PCertReqData', ),
+ '2.23.42.0.39': ('setct-PCertResTBS', ),
+ '2.23.42.0.40': ('setct-BatchAdminReqData', ),
+ '2.23.42.0.41': ('setct-BatchAdminResData', ),
+ '2.23.42.0.42': ('setct-CardCInitResTBS', ),
+ '2.23.42.0.43': ('setct-MeAqCInitResTBS', ),
+ '2.23.42.0.44': ('setct-RegFormResTBS', ),
+ '2.23.42.0.45': ('setct-CertReqData', ),
+ '2.23.42.0.46': ('setct-CertReqTBS', ),
+ '2.23.42.0.47': ('setct-CertResData', ),
+ '2.23.42.0.48': ('setct-CertInqReqTBS', ),
+ '2.23.42.0.49': ('setct-ErrorTBS', ),
+ '2.23.42.0.50': ('setct-PIDualSignedTBE', ),
+ '2.23.42.0.51': ('setct-PIUnsignedTBE', ),
+ '2.23.42.0.52': ('setct-AuthReqTBE', ),
+ '2.23.42.0.53': ('setct-AuthResTBE', ),
+ '2.23.42.0.54': ('setct-AuthResTBEX', ),
+ '2.23.42.0.55': ('setct-AuthTokenTBE', ),
+ '2.23.42.0.56': ('setct-CapTokenTBE', ),
+ '2.23.42.0.57': ('setct-CapTokenTBEX', ),
+ '2.23.42.0.58': ('setct-AcqCardCodeMsgTBE', ),
+ '2.23.42.0.59': ('setct-AuthRevReqTBE', ),
+ '2.23.42.0.60': ('setct-AuthRevResTBE', ),
+ '2.23.42.0.61': ('setct-AuthRevResTBEB', ),
+ '2.23.42.0.62': ('setct-CapReqTBE', ),
+ '2.23.42.0.63': ('setct-CapReqTBEX', ),
+ '2.23.42.0.64': ('setct-CapResTBE', ),
+ '2.23.42.0.65': ('setct-CapRevReqTBE', ),
+ '2.23.42.0.66': ('setct-CapRevReqTBEX', ),
+ '2.23.42.0.67': ('setct-CapRevResTBE', ),
+ '2.23.42.0.68': ('setct-CredReqTBE', ),
+ '2.23.42.0.69': ('setct-CredReqTBEX', ),
+ '2.23.42.0.70': ('setct-CredResTBE', ),
+ '2.23.42.0.71': ('setct-CredRevReqTBE', ),
+ '2.23.42.0.72': ('setct-CredRevReqTBEX', ),
+ '2.23.42.0.73': ('setct-CredRevResTBE', ),
+ '2.23.42.0.74': ('setct-BatchAdminReqTBE', ),
+ '2.23.42.0.75': ('setct-BatchAdminResTBE', ),
+ '2.23.42.0.76': ('setct-RegFormReqTBE', ),
+ '2.23.42.0.77': ('setct-CertReqTBE', ),
+ '2.23.42.0.78': ('setct-CertReqTBEX', ),
+ '2.23.42.0.79': ('setct-CertResTBE', ),
+ '2.23.42.0.80': ('setct-CRLNotificationTBS', ),
+ '2.23.42.0.81': ('setct-CRLNotificationResTBS', ),
+ '2.23.42.0.82': ('setct-BCIDistributionTBS', ),
+ '2.23.42.1': ('message extensions', 'set-msgExt'),
+ '2.23.42.1.1': ('generic cryptogram', 'setext-genCrypt'),
+ '2.23.42.1.3': ('merchant initiated auth', 'setext-miAuth'),
+ '2.23.42.1.4': ('setext-pinSecure', ),
+ '2.23.42.1.5': ('setext-pinAny', ),
+ '2.23.42.1.7': ('setext-track2', ),
+ '2.23.42.1.8': ('additional verification', 'setext-cv'),
+ '2.23.42.3': ('set-attr', ),
+ '2.23.42.3.0': ('setAttr-Cert', ),
+ '2.23.42.3.0.0': ('set-rootKeyThumb', ),
+ '2.23.42.3.0.1': ('set-addPolicy', ),
+ '2.23.42.3.1': ('payment gateway capabilities', 'setAttr-PGWYcap'),
+ '2.23.42.3.2': ('setAttr-TokenType', ),
+ '2.23.42.3.2.1': ('setAttr-Token-EMV', ),
+ '2.23.42.3.2.2': ('setAttr-Token-B0Prime', ),
+ '2.23.42.3.3': ('issuer capabilities', 'setAttr-IssCap'),
+ '2.23.42.3.3.3': ('setAttr-IssCap-CVM', ),
+ '2.23.42.3.3.3.1': ('generate cryptogram', 'setAttr-GenCryptgrm'),
+ '2.23.42.3.3.4': ('setAttr-IssCap-T2', ),
+ '2.23.42.3.3.4.1': ('encrypted track 2', 'setAttr-T2Enc'),
+ '2.23.42.3.3.4.2': ('cleartext track 2', 'setAttr-T2cleartxt'),
+ '2.23.42.3.3.5': ('setAttr-IssCap-Sig', ),
+ '2.23.42.3.3.5.1': ('ICC or token signature', 'setAttr-TokICCsig'),
+ '2.23.42.3.3.5.2': ('secure device signature', 'setAttr-SecDevSig'),
+ '2.23.42.5': ('set-policy', ),
+ '2.23.42.5.0': ('set-policy-root', ),
+ '2.23.42.7': ('certificate extensions', 'set-certExt'),
+ '2.23.42.7.0': ('setCext-hashedRoot', ),
+ '2.23.42.7.1': ('setCext-certType', ),
+ '2.23.42.7.2': ('setCext-merchData', ),
+ '2.23.42.7.3': ('setCext-cCertRequired', ),
+ '2.23.42.7.4': ('setCext-tunneling', ),
+ '2.23.42.7.5': ('setCext-setExt', ),
+ '2.23.42.7.6': ('setCext-setQualf', ),
+ '2.23.42.7.7': ('setCext-PGWYcapabilities', ),
+ '2.23.42.7.8': ('setCext-TokenIdentifier', ),
+ '2.23.42.7.9': ('setCext-Track2Data', ),
+ '2.23.42.7.10': ('setCext-TokenType', ),
+ '2.23.42.7.11': ('setCext-IssuerCapabilities', ),
+ '2.23.42.8': ('set-brand', ),
+ '2.23.42.8.1': ('set-brand-IATA-ATA', ),
+ '2.23.42.8.4': ('set-brand-Visa', ),
+ '2.23.42.8.5': ('set-brand-MasterCard', ),
+ '2.23.42.8.30': ('set-brand-Diners', ),
+ '2.23.42.8.34': ('set-brand-AmericanExpress', ),
+ '2.23.42.8.35': ('set-brand-JCB', ),
+ '2.23.42.8.6011': ('set-brand-Novus', ),
+ '2.23.43': ('wap', ),
+ '2.23.43.1': ('wap-wsg', ),
+ '2.23.43.1.4': ('wap-wsg-idm-ecid', ),
+ '2.23.43.1.4.1': ('wap-wsg-idm-ecid-wtls1', ),
+ '2.23.43.1.4.3': ('wap-wsg-idm-ecid-wtls3', ),
+ '2.23.43.1.4.4': ('wap-wsg-idm-ecid-wtls4', ),
+ '2.23.43.1.4.5': ('wap-wsg-idm-ecid-wtls5', ),
+ '2.23.43.1.4.6': ('wap-wsg-idm-ecid-wtls6', ),
+ '2.23.43.1.4.7': ('wap-wsg-idm-ecid-wtls7', ),
+ '2.23.43.1.4.8': ('wap-wsg-idm-ecid-wtls8', ),
+ '2.23.43.1.4.9': ('wap-wsg-idm-ecid-wtls9', ),
+ '2.23.43.1.4.10': ('wap-wsg-idm-ecid-wtls10', ),
+ '2.23.43.1.4.11': ('wap-wsg-idm-ecid-wtls11', ),
+ '2.23.43.1.4.12': ('wap-wsg-idm-ecid-wtls12', ),
+}
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/basic.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/basic.py
new file mode 100644
index 00000000..11c688d2
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/basic.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+try:
+ import cryptography
+ from cryptography import x509
+
+ # Older versions of cryptography (< 2.1) do not have __hash__ functions for
+ # general name objects (DNSName, IPAddress, ...), while providing overloaded
+ # equality and string representation operations. This makes it impossible to
+ # use them in hash-based data structures such as set or dict. Since we are
+ # actually doing that in x509_certificate, and potentially in other code,
+ # we need to monkey-patch __hash__ for these classes to make sure our code
+ # works fine.
+ if LooseVersion(cryptography.__version__) < LooseVersion('2.1'):
+ # A very simply hash function which relies on the representation
+ # of an object to be implemented. This is the case since at least
+ # cryptography 1.0, see
+ # https://github.com/pyca/cryptography/commit/7a9abce4bff36c05d26d8d2680303a6f64a0e84f
+ def simple_hash(self):
+ return hash(repr(self))
+
+ # The hash functions for the following types were added for cryptography 2.1:
+ # https://github.com/pyca/cryptography/commit/fbfc36da2a4769045f2373b004ddf0aff906cf38
+ x509.DNSName.__hash__ = simple_hash
+ x509.DirectoryName.__hash__ = simple_hash
+ x509.GeneralName.__hash__ = simple_hash
+ x509.IPAddress.__hash__ = simple_hash
+ x509.OtherName.__hash__ = simple_hash
+ x509.RegisteredID.__hash__ = simple_hash
+
+ if LooseVersion(cryptography.__version__) < LooseVersion('1.2'):
+ # The hash functions for the following types were added for cryptography 1.2:
+ # https://github.com/pyca/cryptography/commit/b642deed88a8696e5f01ce6855ccf89985fc35d0
+ # https://github.com/pyca/cryptography/commit/d1b5681f6db2bde7a14625538bd7907b08dfb486
+ x509.RFC822Name.__hash__ = simple_hash
+ x509.UniformResourceIdentifier.__hash__ = simple_hash
+
+ # Test whether we have support for DSA, EC, Ed25519, Ed448, RSA, X25519 and/or X448
+ try:
+ # added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/dsa/
+ import cryptography.hazmat.primitives.asymmetric.dsa
+ CRYPTOGRAPHY_HAS_DSA = True
+ try:
+ # added later in 1.5
+ cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey.sign
+ CRYPTOGRAPHY_HAS_DSA_SIGN = True
+ except AttributeError:
+ CRYPTOGRAPHY_HAS_DSA_SIGN = False
+ except ImportError:
+ CRYPTOGRAPHY_HAS_DSA = False
+ CRYPTOGRAPHY_HAS_DSA_SIGN = False
+ try:
+ # added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed25519/
+ import cryptography.hazmat.primitives.asymmetric.ed25519
+ CRYPTOGRAPHY_HAS_ED25519 = True
+ try:
+ # added with the primitive in 2.6
+ cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.sign
+ CRYPTOGRAPHY_HAS_ED25519_SIGN = True
+ except AttributeError:
+ CRYPTOGRAPHY_HAS_ED25519_SIGN = False
+ except ImportError:
+ CRYPTOGRAPHY_HAS_ED25519 = False
+ CRYPTOGRAPHY_HAS_ED25519_SIGN = False
+ try:
+ # added in 2.6 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ed448/
+ import cryptography.hazmat.primitives.asymmetric.ed448
+ CRYPTOGRAPHY_HAS_ED448 = True
+ try:
+ # added with the primitive in 2.6
+ cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.sign
+ CRYPTOGRAPHY_HAS_ED448_SIGN = True
+ except AttributeError:
+ CRYPTOGRAPHY_HAS_ED448_SIGN = False
+ except ImportError:
+ CRYPTOGRAPHY_HAS_ED448 = False
+ CRYPTOGRAPHY_HAS_ED448_SIGN = False
+ try:
+ # added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/ec/
+ import cryptography.hazmat.primitives.asymmetric.ec
+ CRYPTOGRAPHY_HAS_EC = True
+ try:
+ # added later in 1.5
+ cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey.sign
+ CRYPTOGRAPHY_HAS_EC_SIGN = True
+ except AttributeError:
+ CRYPTOGRAPHY_HAS_EC_SIGN = False
+ except ImportError:
+ CRYPTOGRAPHY_HAS_EC = False
+ CRYPTOGRAPHY_HAS_EC_SIGN = False
+ try:
+ # added in 0.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/
+ import cryptography.hazmat.primitives.asymmetric.rsa
+ CRYPTOGRAPHY_HAS_RSA = True
+ try:
+ # added later in 1.4
+ cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey.sign
+ CRYPTOGRAPHY_HAS_RSA_SIGN = True
+ except AttributeError:
+ CRYPTOGRAPHY_HAS_RSA_SIGN = False
+ except ImportError:
+ CRYPTOGRAPHY_HAS_RSA = False
+ CRYPTOGRAPHY_HAS_RSA_SIGN = False
+ try:
+ # added in 2.0 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x25519/
+ import cryptography.hazmat.primitives.asymmetric.x25519
+ CRYPTOGRAPHY_HAS_X25519 = True
+ try:
+ # added later in 2.5
+ cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.private_bytes
+ CRYPTOGRAPHY_HAS_X25519_FULL = True
+ except AttributeError:
+ CRYPTOGRAPHY_HAS_X25519_FULL = False
+ except ImportError:
+ CRYPTOGRAPHY_HAS_X25519 = False
+ CRYPTOGRAPHY_HAS_X25519_FULL = False
+ try:
+ # added in 2.5 - https://cryptography.io/en/latest/hazmat/primitives/asymmetric/x448/
+ import cryptography.hazmat.primitives.asymmetric.x448
+ CRYPTOGRAPHY_HAS_X448 = True
+ except ImportError:
+ CRYPTOGRAPHY_HAS_X448 = False
+
+ HAS_CRYPTOGRAPHY = True
+except ImportError:
+ # Error handled in the calling module.
+ CRYPTOGRAPHY_HAS_EC = False
+ CRYPTOGRAPHY_HAS_EC_SIGN = False
+ CRYPTOGRAPHY_HAS_ED25519 = False
+ CRYPTOGRAPHY_HAS_ED25519_SIGN = False
+ CRYPTOGRAPHY_HAS_ED448 = False
+ CRYPTOGRAPHY_HAS_ED448_SIGN = False
+ CRYPTOGRAPHY_HAS_DSA = False
+ CRYPTOGRAPHY_HAS_DSA_SIGN = False
+ CRYPTOGRAPHY_HAS_RSA = False
+ CRYPTOGRAPHY_HAS_RSA_SIGN = False
+ CRYPTOGRAPHY_HAS_X25519 = False
+ CRYPTOGRAPHY_HAS_X25519_FULL = False
+ CRYPTOGRAPHY_HAS_X448 = False
+ HAS_CRYPTOGRAPHY = False
+
+
+class OpenSSLObjectError(Exception):
+ pass
+
+
+class OpenSSLBadPassphraseError(OpenSSLObjectError):
+ pass
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_crl.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_crl.py
new file mode 100644
index 00000000..62499e08
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_crl.py
@@ -0,0 +1,114 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+try:
+ from cryptography import x509
+except ImportError:
+ # Error handled in the calling module.
+ pass
+
+from .basic import (
+ HAS_CRYPTOGRAPHY,
+)
+
+from .cryptography_support import (
+ cryptography_decode_name,
+)
+
+from ._obj2txt import (
+ obj2txt,
+)
+
+
+TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
+
+
+if HAS_CRYPTOGRAPHY:
+ REVOCATION_REASON_MAP = {
+ 'unspecified': x509.ReasonFlags.unspecified,
+ 'key_compromise': x509.ReasonFlags.key_compromise,
+ 'ca_compromise': x509.ReasonFlags.ca_compromise,
+ 'affiliation_changed': x509.ReasonFlags.affiliation_changed,
+ 'superseded': x509.ReasonFlags.superseded,
+ 'cessation_of_operation': x509.ReasonFlags.cessation_of_operation,
+ 'certificate_hold': x509.ReasonFlags.certificate_hold,
+ 'privilege_withdrawn': x509.ReasonFlags.privilege_withdrawn,
+ 'aa_compromise': x509.ReasonFlags.aa_compromise,
+ 'remove_from_crl': x509.ReasonFlags.remove_from_crl,
+ }
+ REVOCATION_REASON_MAP_INVERSE = dict()
+ for k, v in REVOCATION_REASON_MAP.items():
+ REVOCATION_REASON_MAP_INVERSE[v] = k
+
+else:
+ REVOCATION_REASON_MAP = dict()
+ REVOCATION_REASON_MAP_INVERSE = dict()
+
+
+def cryptography_decode_revoked_certificate(cert):
+ result = {
+ 'serial_number': cert.serial_number,
+ 'revocation_date': cert.revocation_date,
+ 'issuer': None,
+ 'issuer_critical': False,
+ 'reason': None,
+ 'reason_critical': False,
+ 'invalidity_date': None,
+ 'invalidity_date_critical': False,
+ }
+ try:
+ ext = cert.extensions.get_extension_for_class(x509.CertificateIssuer)
+ result['issuer'] = list(ext.value)
+ result['issuer_critical'] = ext.critical
+ except x509.ExtensionNotFound:
+ pass
+ try:
+ ext = cert.extensions.get_extension_for_class(x509.CRLReason)
+ result['reason'] = ext.value.reason
+ result['reason_critical'] = ext.critical
+ except x509.ExtensionNotFound:
+ pass
+ try:
+ ext = cert.extensions.get_extension_for_class(x509.InvalidityDate)
+ result['invalidity_date'] = ext.value.invalidity_date
+ result['invalidity_date_critical'] = ext.critical
+ except x509.ExtensionNotFound:
+ pass
+ return result
+
+
+def cryptography_dump_revoked(entry, idn_rewrite='ignore'):
+ return {
+ 'serial_number': entry['serial_number'],
+ 'revocation_date': entry['revocation_date'].strftime(TIMESTAMP_FORMAT),
+ 'issuer':
+ [cryptography_decode_name(issuer, idn_rewrite=idn_rewrite) for issuer in entry['issuer']]
+ if entry['issuer'] is not None else None,
+ 'issuer_critical': entry['issuer_critical'],
+ 'reason': REVOCATION_REASON_MAP_INVERSE.get(entry['reason']) if entry['reason'] is not None else None,
+ 'reason_critical': entry['reason_critical'],
+ 'invalidity_date':
+ entry['invalidity_date'].strftime(TIMESTAMP_FORMAT)
+ if entry['invalidity_date'] is not None else None,
+ 'invalidity_date_critical': entry['invalidity_date_critical'],
+ }
+
+
+def cryptography_get_signature_algorithm_oid_from_crl(crl):
+ try:
+ return crl.signature_algorithm_oid
+ except AttributeError:
+ # Older cryptography versions do not have signature_algorithm_oid yet
+ dotted = obj2txt(
+ crl._backend._lib,
+ crl._backend._ffi,
+ crl._x509_crl.sig_alg.algorithm
+ )
+ return x509.oid.ObjectIdentifier(dotted)
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_support.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_support.py
new file mode 100644
index 00000000..fde69199
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/cryptography_support.py
@@ -0,0 +1,809 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import base64
+import binascii
+import re
+import sys
+import traceback
+
+from ansible.module_utils.common.text.converters import to_text, to_bytes, to_native
+from ansible.module_utils.six.moves.urllib.parse import urlparse, urlunparse, ParseResult
+
+from ._asn1 import serialize_asn1_string_as_der
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+try:
+ import cryptography
+ from cryptography import x509
+ from cryptography.exceptions import InvalidSignature
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives import serialization
+ from cryptography.hazmat.primitives.asymmetric import padding
+ import ipaddress
+except ImportError:
+ # Error handled in the calling module.
+ pass
+
+try:
+ import cryptography.hazmat.primitives.asymmetric.rsa
+except ImportError:
+ pass
+try:
+ import cryptography.hazmat.primitives.asymmetric.ec
+except ImportError:
+ pass
+try:
+ import cryptography.hazmat.primitives.asymmetric.dsa
+except ImportError:
+ pass
+try:
+ import cryptography.hazmat.primitives.asymmetric.ed25519
+except ImportError:
+ pass
+try:
+ import cryptography.hazmat.primitives.asymmetric.ed448
+except ImportError:
+ pass
+
+try:
+ # This is a separate try/except since this is only present in cryptography 2.5 or newer
+ from cryptography.hazmat.primitives.serialization.pkcs12 import (
+ load_key_and_certificates as _load_key_and_certificates,
+ )
+except ImportError:
+ # Error handled in the calling module.
+ _load_key_and_certificates = None
+
+try:
+ # This is a separate try/except since this is only present in cryptography 36.0.0 or newer
+ from cryptography.hazmat.primitives.serialization.pkcs12 import (
+ load_pkcs12 as _load_pkcs12,
+ )
+except ImportError:
+ # Error handled in the calling module.
+ _load_pkcs12 = None
+
+try:
+ import idna
+
+ HAS_IDNA = True
+except ImportError:
+ HAS_IDNA = False
+ IDNA_IMP_ERROR = traceback.format_exc()
+
+from ansible.module_utils.basic import missing_required_lib
+
+from .basic import (
+ CRYPTOGRAPHY_HAS_DSA_SIGN,
+ CRYPTOGRAPHY_HAS_EC_SIGN,
+ CRYPTOGRAPHY_HAS_ED25519,
+ CRYPTOGRAPHY_HAS_ED25519_SIGN,
+ CRYPTOGRAPHY_HAS_ED448,
+ CRYPTOGRAPHY_HAS_ED448_SIGN,
+ CRYPTOGRAPHY_HAS_RSA_SIGN,
+ CRYPTOGRAPHY_HAS_X25519,
+ CRYPTOGRAPHY_HAS_X25519_FULL,
+ CRYPTOGRAPHY_HAS_X448,
+ OpenSSLObjectError,
+)
+
+from ._objects import (
+ OID_LOOKUP,
+ OID_MAP,
+ NORMALIZE_NAMES_SHORT,
+ NORMALIZE_NAMES,
+)
+
+from ._obj2txt import obj2txt
+
+
+DOTTED_OID = re.compile(r'^\d+(?:\.\d+)+$')
+
+
+def cryptography_get_extensions_from_cert(cert):
+ result = dict()
+ try:
+ # Since cryptography will not give us the DER value for an extension
+ # (that is only stored for unrecognized extensions), we have to re-do
+ # the extension parsing outselves.
+ backend = default_backend()
+ try:
+ # For certain old versions of cryptography, backend is a MultiBackend object,
+ # which has no _lib attribute. In that case, revert to the old approach.
+ backend._lib
+ except AttributeError:
+ backend = cert._backend
+
+ x509_obj = cert._x509
+ # With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does
+ # not allow to get the raw value of an extension, so we have to use this ugly hack:
+ exts = list(cert.extensions)
+
+ for i in range(backend._lib.X509_get_ext_count(x509_obj)):
+ ext = backend._lib.X509_get_ext(x509_obj, i)
+ if ext == backend._ffi.NULL:
+ continue
+ crit = backend._lib.X509_EXTENSION_get_critical(ext)
+ data = backend._lib.X509_EXTENSION_get_data(ext)
+ backend.openssl_assert(data != backend._ffi.NULL)
+ der = backend._ffi.buffer(data.data, data.length)[:]
+ entry = dict(
+ critical=(crit == 1),
+ value=to_native(base64.b64encode(der)),
+ )
+ try:
+ oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
+ except AttributeError:
+ oid = exts[i].oid.dotted_string
+ result[oid] = entry
+
+ except Exception:
+ # In case the above method breaks, we likely have cryptography 36.0.0 or newer.
+ # Use it's public_bytes() feature in that case. We will later switch this around
+ # so that this code will be the default, but for now this will act as a fallback
+ # since it will re-serialize de-serialized data, which can be different (if the
+ # original data was not canonicalized) from what was contained in the certificate.
+ for ext in cert.extensions:
+ result[ext.oid.dotted_string] = dict(
+ critical=ext.critical,
+ value=to_native(base64.b64encode(ext.value.public_bytes())),
+ )
+
+ return result
+
+
+def cryptography_get_extensions_from_csr(csr):
+ result = dict()
+ try:
+ # Since cryptography will not give us the DER value for an extension
+ # (that is only stored for unrecognized extensions), we have to re-do
+ # the extension parsing outselves.
+ backend = default_backend()
+ try:
+ # For certain old versions of cryptography, backend is a MultiBackend object,
+ # which has no _lib attribute. In that case, revert to the old approach.
+ backend._lib
+ except AttributeError:
+ backend = csr._backend
+
+ extensions = backend._lib.X509_REQ_get_extensions(csr._x509_req)
+ extensions = backend._ffi.gc(
+ extensions,
+ lambda ext: backend._lib.sk_X509_EXTENSION_pop_free(
+ ext,
+ backend._ffi.addressof(backend._lib._original_lib, "X509_EXTENSION_free")
+ )
+ )
+
+ # With cryptography 35.0.0, we can no longer use obj2txt. Unfortunately it still does
+ # not allow to get the raw value of an extension, so we have to use this ugly hack:
+ exts = list(csr.extensions)
+
+ for i in range(backend._lib.sk_X509_EXTENSION_num(extensions)):
+ ext = backend._lib.sk_X509_EXTENSION_value(extensions, i)
+ if ext == backend._ffi.NULL:
+ continue
+ crit = backend._lib.X509_EXTENSION_get_critical(ext)
+ data = backend._lib.X509_EXTENSION_get_data(ext)
+ backend.openssl_assert(data != backend._ffi.NULL)
+ der = backend._ffi.buffer(data.data, data.length)[:]
+ entry = dict(
+ critical=(crit == 1),
+ value=to_native(base64.b64encode(der)),
+ )
+ try:
+ oid = obj2txt(backend._lib, backend._ffi, backend._lib.X509_EXTENSION_get_object(ext))
+ except AttributeError:
+ oid = exts[i].oid.dotted_string
+ result[oid] = entry
+
+ except Exception:
+ # In case the above method breaks, we likely have cryptography 36.0.0 or newer.
+ # Use it's public_bytes() feature in that case. We will later switch this around
+ # so that this code will be the default, but for now this will act as a fallback
+ # since it will re-serialize de-serialized data, which can be different (if the
+ # original data was not canonicalized) from what was contained in the CSR.
+ for ext in csr.extensions:
+ result[ext.oid.dotted_string] = dict(
+ critical=ext.critical,
+ value=to_native(base64.b64encode(ext.value.public_bytes())),
+ )
+
+ return result
+
+
+def cryptography_name_to_oid(name):
+ dotted = OID_LOOKUP.get(name)
+ if dotted is None:
+ if DOTTED_OID.match(name):
+ return x509.oid.ObjectIdentifier(name)
+ raise OpenSSLObjectError('Cannot find OID for "{0}"'.format(name))
+ return x509.oid.ObjectIdentifier(dotted)
+
+
+def cryptography_oid_to_name(oid, short=False):
+ dotted_string = oid.dotted_string
+ names = OID_MAP.get(dotted_string)
+ if names:
+ name = names[0]
+ else:
+ name = oid._name
+ if name == 'Unknown OID':
+ name = dotted_string
+ if short:
+ return NORMALIZE_NAMES_SHORT.get(name, name)
+ else:
+ return NORMALIZE_NAMES.get(name, name)
+
+
+def _get_hex(bytesstr):
+ if bytesstr is None:
+ return bytesstr
+ data = binascii.hexlify(bytesstr)
+ data = to_text(b':'.join(data[i:i + 2] for i in range(0, len(data), 2)))
+ return data
+
+
+def _parse_hex(bytesstr):
+ if bytesstr is None:
+ return bytesstr
+ data = ''.join([('0' * (2 - len(p)) + p) if len(p) < 2 else p for p in to_text(bytesstr).split(':')])
+ data = binascii.unhexlify(data)
+ return data
+
+
+DN_COMPONENT_START_RE = re.compile(b'^ *([a-zA-z0-9.]+) *= *')
+DN_HEX_LETTER = b'0123456789abcdef'
+
+
+if sys.version_info[0] < 3:
+ _int_to_byte = chr
+else:
+ def _int_to_byte(value):
+ return bytes((value, ))
+
+
+def _parse_dn_component(name, sep=b',', decode_remainder=True):
+ m = DN_COMPONENT_START_RE.match(name)
+ if not m:
+ raise OpenSSLObjectError(u'cannot start part in "{0}"'.format(to_text(name)))
+ oid = cryptography_name_to_oid(to_text(m.group(1)))
+ idx = len(m.group(0))
+ decoded_name = []
+ sep_str = sep + b'\\'
+ if decode_remainder:
+ length = len(name)
+ if length > idx and name[idx:idx + 1] == b'#':
+ # Decoding a hex string
+ idx += 1
+ while idx + 1 < length:
+ ch1 = name[idx:idx + 1]
+ ch2 = name[idx + 1:idx + 2]
+ idx1 = DN_HEX_LETTER.find(ch1.lower())
+ idx2 = DN_HEX_LETTER.find(ch2.lower())
+ if idx1 < 0 or idx2 < 0:
+ raise OpenSSLObjectError(u'Invalid hex sequence entry "{0}"'.format(to_text(ch1 + ch2)))
+ idx += 2
+ decoded_name.append(_int_to_byte(idx1 * 16 + idx2))
+ else:
+ # Decoding a regular string
+ while idx < length:
+ i = idx
+ while i < length and name[i:i + 1] not in sep_str:
+ i += 1
+ if i > idx:
+ decoded_name.append(name[idx:i])
+ idx = i
+ while idx + 1 < length and name[idx:idx + 1] == b'\\':
+ ch = name[idx + 1:idx + 2]
+ idx1 = DN_HEX_LETTER.find(ch.lower())
+ if idx1 >= 0:
+ if idx + 2 >= length:
+ raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" incomplete at end of string'.format(to_text(ch)))
+ ch2 = name[idx + 2:idx + 3]
+ idx2 = DN_HEX_LETTER.find(ch2.lower())
+ if idx2 < 0:
+ raise OpenSSLObjectError(u'Hex escape sequence "\\{0}" has invalid second letter'.format(to_text(ch + ch2)))
+ ch = _int_to_byte(idx1 * 16 + idx2)
+ idx += 1
+ idx += 2
+ decoded_name.append(ch)
+ if idx < length and name[idx:idx + 1] == sep:
+ break
+ else:
+ decoded_name.append(name[idx:])
+ idx = len(name)
+ return x509.NameAttribute(oid, to_text(b''.join(decoded_name))), name[idx:]
+
+
+def _parse_dn(name):
+ '''
+ Parse a Distinguished Name.
+
+ Can be of the form ``CN=Test, O = Something`` or ``CN = Test,O= Something``.
+ '''
+ original_name = name
+ name = name.lstrip()
+ sep = b','
+ if name.startswith(b'/'):
+ sep = b'/'
+ name = name[1:]
+ result = []
+ while name:
+ try:
+ attribute, name = _parse_dn_component(name, sep=sep)
+ except OpenSSLObjectError as e:
+ raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": {1}'.format(to_text(original_name), e))
+ result.append(attribute)
+ if name:
+ if name[0:1] != sep or len(name) < 2:
+ raise OpenSSLObjectError(u'Error while parsing distinguished name "{0}": unexpected end of string'.format(to_text(original_name)))
+ name = name[1:]
+ return result
+
+
+def cryptography_parse_relative_distinguished_name(rdn):
+ names = []
+ for part in rdn:
+ try:
+ names.append(_parse_dn_component(to_bytes(part), decode_remainder=False)[0])
+ except OpenSSLObjectError as e:
+ raise OpenSSLObjectError(u'Error while parsing relative distinguished name "{0}": {1}'.format(part, e))
+ return cryptography.x509.RelativeDistinguishedName(names)
+
+
+def _is_ascii(value):
+ '''Check whether the Unicode string `value` contains only ASCII characters.'''
+ try:
+ value.encode("ascii")
+ return True
+ except UnicodeEncodeError:
+ return False
+
+
+def _adjust_idn(value, idn_rewrite):
+ if idn_rewrite == 'ignore' or not value:
+ return value
+ if idn_rewrite == 'idna' and _is_ascii(value):
+ return value
+ if idn_rewrite not in ('idna', 'unicode'):
+ raise ValueError('Invalid value for idn_rewrite: "{0}"'.format(idn_rewrite))
+ if not HAS_IDNA:
+ raise OpenSSLObjectError(
+ missing_required_lib('idna', reason='to transform {what} DNS name "{name}" to {dest}'.format(
+ name=value,
+ what='IDNA' if idn_rewrite == 'unicode' else 'Unicode',
+ dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA',
+ )))
+ # Since IDNA does not like '*' or empty labels (except one empty label at the end),
+ # we split and let IDNA only handle labels that are neither empty or '*'.
+ parts = value.split(u'.')
+ for index, part in enumerate(parts):
+ if part in (u'', u'*'):
+ continue
+ try:
+ if idn_rewrite == 'idna':
+ parts[index] = idna.encode(part).decode('ascii')
+ elif idn_rewrite == 'unicode' and part.startswith(u'xn--'):
+ parts[index] = idna.decode(part)
+ except idna.IDNAError as exc2008:
+ try:
+ if idn_rewrite == 'idna':
+ parts[index] = part.encode('idna').decode('ascii')
+ elif idn_rewrite == 'unicode' and part.startswith(u'xn--'):
+ parts[index] = part.encode('ascii').decode('idna')
+ except Exception as exc2003:
+ raise OpenSSLObjectError(
+ u'Error while transforming part "{part}" of {what} DNS name "{name}" to {dest}.'
+ u' IDNA2008 transformation resulted in "{exc2008}", IDNA2003 transformation resulted in "{exc2003}".'.format(
+ part=part,
+ name=value,
+ what='IDNA' if idn_rewrite == 'unicode' else 'Unicode',
+ dest='Unicode' if idn_rewrite == 'unicode' else 'IDNA',
+ exc2003=exc2003,
+ exc2008=exc2008,
+ ))
+ return u'.'.join(parts)
+
+
+def _adjust_idn_email(value, idn_rewrite):
+ idx = value.find(u'@')
+ if idx < 0:
+ return value
+ return u'{0}@{1}'.format(value[:idx], _adjust_idn(value[idx + 1:], idn_rewrite))
+
+
+def _adjust_idn_url(value, idn_rewrite):
+ url = urlparse(value)
+ host = _adjust_idn(url.hostname, idn_rewrite)
+ if url.username is not None and url.password is not None:
+ host = u'{0}:{1}@{2}'.format(url.username, url.password, host)
+ elif url.username is not None:
+ host = u'{0}@{1}'.format(url.username, host)
+ if url.port is not None:
+ host = u'{0}:{1}'.format(host, url.port)
+ return urlunparse(
+ ParseResult(scheme=url.scheme, netloc=host, path=url.path, params=url.params, query=url.query, fragment=url.fragment))
+
+
+def cryptography_get_name(name, what='Subject Alternative Name'):
+ '''
+ Given a name string, returns a cryptography x509.GeneralName object.
+ Raises an OpenSSLObjectError if the name is unknown or cannot be parsed.
+ '''
+ try:
+ if name.startswith('DNS:'):
+ return x509.DNSName(_adjust_idn(to_text(name[4:]), 'idna'))
+ if name.startswith('IP:'):
+ address = to_text(name[3:])
+ if '/' in address:
+ return x509.IPAddress(ipaddress.ip_network(address))
+ return x509.IPAddress(ipaddress.ip_address(address))
+ if name.startswith('email:'):
+ return x509.RFC822Name(_adjust_idn_email(to_text(name[6:]), 'idna'))
+ if name.startswith('URI:'):
+ return x509.UniformResourceIdentifier(_adjust_idn_url(to_text(name[4:]), 'idna'))
+ if name.startswith('RID:'):
+ m = re.match(r'^([0-9]+(?:\.[0-9]+)*)$', to_text(name[4:]))
+ if not m:
+ raise OpenSSLObjectError('Cannot parse {what} "{name}"'.format(name=name, what=what))
+ return x509.RegisteredID(x509.oid.ObjectIdentifier(m.group(1)))
+ if name.startswith('otherName:'):
+ # otherName can either be a raw ASN.1 hex string or in the format that OpenSSL works with.
+ m = re.match(r'^([0-9]+(?:\.[0-9]+)*);([0-9a-fA-F]{1,2}(?::[0-9a-fA-F]{1,2})*)$', to_text(name[10:]))
+ if m:
+ return x509.OtherName(x509.oid.ObjectIdentifier(m.group(1)), _parse_hex(m.group(2)))
+
+ # See https://www.openssl.org/docs/man1.0.2/man5/x509v3_config.html - Subject Alternative Name for more
+ # defailts on the format expected.
+ name = to_text(name[10:], errors='surrogate_or_strict')
+ if ';' not in name:
+ raise OpenSSLObjectError('Cannot parse {what} otherName "{name}", must be in the '
+ 'format "otherName:<OID>;<ASN.1 OpenSSL Encoded String>" or '
+ '"otherName:<OID>;<hex string>"'.format(name=name, what=what))
+
+ oid, value = name.split(';', 1)
+ b_value = serialize_asn1_string_as_der(value)
+ return x509.OtherName(x509.ObjectIdentifier(oid), b_value)
+ if name.startswith('dirName:'):
+ return x509.DirectoryName(x509.Name(reversed(_parse_dn(to_bytes(name[8:])))))
+ except Exception as e:
+ raise OpenSSLObjectError('Cannot parse {what} "{name}": {error}'.format(name=name, what=what, error=e))
+ if ':' not in name:
+ raise OpenSSLObjectError('Cannot parse {what} "{name}" (forgot "DNS:" prefix?)'.format(name=name, what=what))
+ raise OpenSSLObjectError('Cannot parse {what} "{name}" (potentially unsupported by cryptography backend)'.format(name=name, what=what))
+
+
+def _dn_escape_value(value):
+ '''
+ Escape Distinguished Name's attribute value.
+ '''
+ value = value.replace(u'\\', u'\\\\')
+ for ch in [u',', u'+', u'<', u'>', u';', u'"']:
+ value = value.replace(ch, u'\\%s' % ch)
+ value = value.replace(u'\0', u'\\00')
+ if value.startswith((u' ', u'#')):
+ value = u'\\%s' % value[0] + value[1:]
+ if value.endswith(u' '):
+ value = value[:-1] + u'\\ '
+ return value
+
+
+def cryptography_decode_name(name, idn_rewrite='ignore'):
+ '''
+ Given a cryptography x509.GeneralName object, returns a string.
+ Raises an OpenSSLObjectError if the name is not supported.
+ '''
+ if idn_rewrite not in ('ignore', 'idna', 'unicode'):
+ raise AssertionError('idn_rewrite must be one of "ignore", "idna", or "unicode"')
+ if isinstance(name, x509.DNSName):
+ return u'DNS:{0}'.format(_adjust_idn(name.value, idn_rewrite))
+ if isinstance(name, x509.IPAddress):
+ if isinstance(name.value, (ipaddress.IPv4Network, ipaddress.IPv6Network)):
+ return u'IP:{0}/{1}'.format(name.value.network_address.compressed, name.value.prefixlen)
+ return u'IP:{0}'.format(name.value.compressed)
+ if isinstance(name, x509.RFC822Name):
+ return u'email:{0}'.format(_adjust_idn_email(name.value, idn_rewrite))
+ if isinstance(name, x509.UniformResourceIdentifier):
+ return u'URI:{0}'.format(_adjust_idn_url(name.value, idn_rewrite))
+ if isinstance(name, x509.DirectoryName):
+ # According to https://datatracker.ietf.org/doc/html/rfc4514.html#section-2.1 the
+ # list needs to be reversed, and joined by commas
+ return u'dirName:' + ','.join([
+ u'{0}={1}'.format(to_text(cryptography_oid_to_name(attribute.oid, short=True)), _dn_escape_value(attribute.value))
+ for attribute in reversed(list(name.value))
+ ])
+ if isinstance(name, x509.RegisteredID):
+ return u'RID:{0}'.format(name.value.dotted_string)
+ if isinstance(name, x509.OtherName):
+ return u'otherName:{0};{1}'.format(name.type_id.dotted_string, _get_hex(name.value))
+ raise OpenSSLObjectError('Cannot decode name "{0}"'.format(name))
+
+
+def _cryptography_get_keyusage(usage):
+ '''
+ Given a key usage identifier string, returns the parameter name used by cryptography's x509.KeyUsage().
+ Raises an OpenSSLObjectError if the identifier is unknown.
+ '''
+ if usage in ('Digital Signature', 'digitalSignature'):
+ return 'digital_signature'
+ if usage in ('Non Repudiation', 'nonRepudiation'):
+ return 'content_commitment'
+ if usage in ('Key Encipherment', 'keyEncipherment'):
+ return 'key_encipherment'
+ if usage in ('Data Encipherment', 'dataEncipherment'):
+ return 'data_encipherment'
+ if usage in ('Key Agreement', 'keyAgreement'):
+ return 'key_agreement'
+ if usage in ('Certificate Sign', 'keyCertSign'):
+ return 'key_cert_sign'
+ if usage in ('CRL Sign', 'cRLSign'):
+ return 'crl_sign'
+ if usage in ('Encipher Only', 'encipherOnly'):
+ return 'encipher_only'
+ if usage in ('Decipher Only', 'decipherOnly'):
+ return 'decipher_only'
+ raise OpenSSLObjectError('Unknown key usage "{0}"'.format(usage))
+
+
+def cryptography_parse_key_usage_params(usages):
+ '''
+ Given a list of key usage identifier strings, returns the parameters for cryptography's x509.KeyUsage().
+ Raises an OpenSSLObjectError if an identifier is unknown.
+ '''
+ params = dict(
+ digital_signature=False,
+ content_commitment=False,
+ key_encipherment=False,
+ data_encipherment=False,
+ key_agreement=False,
+ key_cert_sign=False,
+ crl_sign=False,
+ encipher_only=False,
+ decipher_only=False,
+ )
+ for usage in usages:
+ params[_cryptography_get_keyusage(usage)] = True
+ return params
+
+
+def cryptography_get_basic_constraints(constraints):
+ '''
+ Given a list of constraints, returns a tuple (ca, path_length).
+ Raises an OpenSSLObjectError if a constraint is unknown or cannot be parsed.
+ '''
+ ca = False
+ path_length = None
+ if constraints:
+ for constraint in constraints:
+ if constraint.startswith('CA:'):
+ if constraint == 'CA:TRUE':
+ ca = True
+ elif constraint == 'CA:FALSE':
+ ca = False
+ else:
+ raise OpenSSLObjectError('Unknown basic constraint value "{0}" for CA'.format(constraint[3:]))
+ elif constraint.startswith('pathlen:'):
+ v = constraint[len('pathlen:'):]
+ try:
+ path_length = int(v)
+ except Exception as e:
+ raise OpenSSLObjectError('Cannot parse path length constraint "{0}" ({1})'.format(v, e))
+ else:
+ raise OpenSSLObjectError('Unknown basic constraint "{0}"'.format(constraint))
+ return ca, path_length
+
+
+def cryptography_key_needs_digest_for_signing(key):
+ '''Tests whether the given private key requires a digest algorithm for signing.
+
+ Ed25519 and Ed448 keys do not; they need None to be passed as the digest algorithm.
+ '''
+ if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
+ return False
+ if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
+ return False
+ return True
+
+
+def _compare_public_keys(key1, key2, clazz):
+ a = isinstance(key1, clazz)
+ b = isinstance(key2, clazz)
+ if not (a or b):
+ return None
+ if not a or not b:
+ return False
+ a = key1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
+ b = key2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
+ return a == b
+
+
+def cryptography_compare_public_keys(key1, key2):
+ '''Tests whether two public keys are the same.
+
+ Needs special logic for Ed25519 and Ed448 keys, since they do not have public_numbers().
+ '''
+ if CRYPTOGRAPHY_HAS_ED25519:
+ res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey)
+ if res is not None:
+ return res
+ if CRYPTOGRAPHY_HAS_ED448:
+ res = _compare_public_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey)
+ if res is not None:
+ return res
+ return key1.public_numbers() == key2.public_numbers()
+
+
+def _compare_private_keys(key1, key2, clazz, has_no_private_bytes=False):
+ a = isinstance(key1, clazz)
+ b = isinstance(key2, clazz)
+ if not (a or b):
+ return None
+ if not a or not b:
+ return False
+ if has_no_private_bytes:
+ # We do not have the private_bytes() function - compare associated public keys
+ return cryptography_compare_public_keys(a.public_key(), b.public_key())
+ encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption()
+ a = key1.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm)
+ b = key2.private_bytes(serialization.Encoding.Raw, serialization.PrivateFormat.Raw, encryption_algorithm=encryption_algorithm)
+ return a == b
+
+
+def cryptography_compare_private_keys(key1, key2):
+ '''Tests whether two private keys are the same.
+
+ Needs special logic for Ed25519, X25519, and Ed448 keys, since they do not have private_numbers().
+ '''
+ if CRYPTOGRAPHY_HAS_ED25519:
+ res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey)
+ if res is not None:
+ return res
+ if CRYPTOGRAPHY_HAS_X25519:
+ res = _compare_private_keys(
+ key1, key2, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey, has_no_private_bytes=not CRYPTOGRAPHY_HAS_X25519_FULL)
+ if res is not None:
+ return res
+ if CRYPTOGRAPHY_HAS_ED448:
+ res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey)
+ if res is not None:
+ return res
+ if CRYPTOGRAPHY_HAS_X448:
+ res = _compare_private_keys(key1, key2, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey)
+ if res is not None:
+ return res
+ return key1.private_numbers() == key2.private_numbers()
+
+
+def cryptography_serial_number_of_cert(cert):
+ '''Returns cert.serial_number.
+
+ Also works for old versions of cryptography.
+ '''
+ try:
+ return cert.serial_number
+ except AttributeError:
+ # The property was called "serial" before cryptography 1.4
+ return cert.serial
+
+
+def parse_pkcs12(pkcs12_bytes, passphrase=None):
+ '''Returns a tuple (private_key, certificate, additional_certificates, friendly_name).
+ '''
+ if _load_pkcs12 is None and _load_key_and_certificates is None:
+ raise ValueError('neither load_pkcs12() nor load_key_and_certificates() present in the current cryptography version')
+
+ if passphrase is not None:
+ passphrase = to_bytes(passphrase)
+
+ # Main code for cryptography 36.0.0 and forward
+ if _load_pkcs12 is not None:
+ return _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase)
+
+ if LooseVersion(cryptography.__version__) >= LooseVersion('35.0'):
+ return _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase)
+
+ return _parse_pkcs12_legacy(pkcs12_bytes, passphrase)
+
+
+def _parse_pkcs12_36_0_0(pkcs12_bytes, passphrase=None):
+ # Requires cryptography 36.0.0 or newer
+ pkcs12 = _load_pkcs12(pkcs12_bytes, passphrase)
+ additional_certificates = [cert.certificate for cert in pkcs12.additional_certs]
+ private_key = pkcs12.key
+ certificate = None
+ friendly_name = None
+ if pkcs12.cert:
+ certificate = pkcs12.cert.certificate
+ friendly_name = pkcs12.cert.friendly_name
+ return private_key, certificate, additional_certificates, friendly_name
+
+
+def _parse_pkcs12_35_0_0(pkcs12_bytes, passphrase=None):
+ # Backwards compatibility code for cryptography 35.x
+ private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
+
+ friendly_name = None
+ if certificate:
+ # See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
+ backend = default_backend()
+
+ # This code basically does what load_key_and_certificates() does, but without error-checking.
+ # Since load_key_and_certificates succeeded, it should not fail.
+ pkcs12 = backend._ffi.gc(
+ backend._lib.d2i_PKCS12_bio(backend._bytes_to_bio(pkcs12_bytes).bio, backend._ffi.NULL),
+ backend._lib.PKCS12_free)
+ certificate_x509_ptr = backend._ffi.new("X509 **")
+ with backend._zeroed_null_terminated_buf(to_bytes(passphrase) if passphrase is not None else None) as passphrase_buffer:
+ backend._lib.PKCS12_parse(
+ pkcs12,
+ passphrase_buffer,
+ backend._ffi.new("EVP_PKEY **"),
+ certificate_x509_ptr,
+ backend._ffi.new("Cryptography_STACK_OF_X509 **"))
+ if certificate_x509_ptr[0] != backend._ffi.NULL:
+ maybe_name = backend._lib.X509_alias_get0(certificate_x509_ptr[0], backend._ffi.NULL)
+ if maybe_name != backend._ffi.NULL:
+ friendly_name = backend._ffi.string(maybe_name)
+
+ return private_key, certificate, additional_certificates, friendly_name
+
+
+def _parse_pkcs12_legacy(pkcs12_bytes, passphrase=None):
+ # Backwards compatibility code for cryptography < 35.0.0
+ private_key, certificate, additional_certificates = _load_key_and_certificates(pkcs12_bytes, passphrase)
+
+ friendly_name = None
+ if certificate:
+ # See https://github.com/pyca/cryptography/issues/5760#issuecomment-842687238
+ backend = certificate._backend
+ maybe_name = backend._lib.X509_alias_get0(certificate._x509, backend._ffi.NULL)
+ if maybe_name != backend._ffi.NULL:
+ friendly_name = backend._ffi.string(maybe_name)
+ return private_key, certificate, additional_certificates, friendly_name
+
+
+def cryptography_verify_signature(signature, data, hash_algorithm, signer_public_key):
+ '''
+ Check whether the given signature of the given data was signed by the given public key object.
+ '''
+ try:
+ if CRYPTOGRAPHY_HAS_RSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
+ signer_public_key.verify(signature, data, padding.PKCS1v15(), hash_algorithm)
+ return True
+ if CRYPTOGRAPHY_HAS_EC_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
+ signer_public_key.verify(signature, data, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(hash_algorithm))
+ return True
+ if CRYPTOGRAPHY_HAS_DSA_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey):
+ signer_public_key.verify(signature, data, hash_algorithm)
+ return True
+ if CRYPTOGRAPHY_HAS_ED25519_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
+ signer_public_key.verify(signature, data)
+ return True
+ if CRYPTOGRAPHY_HAS_ED448_SIGN and isinstance(signer_public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
+ signer_public_key.verify(signature, data)
+ return True
+ raise OpenSSLObjectError(u'Unsupported public key type {0}'.format(type(signer_public_key)))
+ except InvalidSignature:
+ return False
+
+
+def cryptography_verify_certificate_signature(certificate, signer_public_key):
+ '''
+ Check whether the given X509 certificate object was signed by the given public key object.
+ '''
+ return cryptography_verify_signature(
+ certificate.signature,
+ certificate.tbs_certificate_bytes,
+ certificate.signature_hash_algorithm,
+ signer_public_key
+ )
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/math.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/math.py
new file mode 100644
index 00000000..1cfe38b9
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/math.py
@@ -0,0 +1,70 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import sys
+
+
+def binary_exp_mod(f, e, m):
+ '''Computes f^e mod m in O(log e) multiplications modulo m.'''
+ # Compute len_e = floor(log_2(e))
+ len_e = -1
+ x = e
+ while x > 0:
+ x >>= 1
+ len_e += 1
+ # Compute f**e mod m
+ result = 1
+ for k in range(len_e, -1, -1):
+ result = (result * result) % m
+ if ((e >> k) & 1) != 0:
+ result = (result * f) % m
+ return result
+
+
+def simple_gcd(a, b):
+ '''Compute GCD of its two inputs.'''
+ while b != 0:
+ a, b = b, a % b
+ return a
+
+
+def quick_is_not_prime(n):
+ '''Does some quick checks to see if we can poke a hole into the primality of n.
+
+ A result of `False` does **not** mean that the number is prime; it just means
+ that we could not detect quickly whether it is not prime.
+ '''
+ if n <= 2:
+ return True
+ # The constant in the next line is the product of all primes < 200
+ if simple_gcd(n, 7799922041683461553249199106329813876687996789903550945093032474868511536164700810) > 1:
+ return True
+ # TODO: maybe do some iterations of Miller-Rabin to increase confidence
+ # (https://en.wikipedia.org/wiki/Miller%E2%80%93Rabin_primality_test)
+ return False
+
+
+python_version = (sys.version_info[0], sys.version_info[1])
+if python_version >= (2, 7) or python_version >= (3, 1):
+ # Ansible still supports Python 2.6 on remote nodes
+ def count_bits(no):
+ no = abs(no)
+ if no == 0:
+ return 0
+ return no.bit_length()
+else:
+ # Slow, but works
+ def count_bits(no):
+ no = abs(no)
+ count = 0
+ while no > 0:
+ no >>= 1
+ count += 1
+ return count
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate.py
new file mode 100644
index 00000000..7a56d7e9
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate.py
@@ -0,0 +1,354 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+import traceback
+
+from ansible.module_utils import six
+from ansible.module_utils.basic import missing_required_lib
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+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,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
+ get_certificate_info,
+)
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
+
+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.ignore_timestamps = module.params['ignore_timestamps']
+ 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
+
+ self.diff_before = self._get_info(None)
+ self.diff_after = self._get_info(None)
+
+ def _get_info(self, data):
+ if data is None:
+ return dict()
+ try:
+ result = get_certificate_info(self.module, self.backend, data, prefer_one_fingerprint=True)
+ result['can_parse_certificate'] = True
+ return result
+ except Exception as exc:
+ return dict(can_parse_certificate=False)
+
+ @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
+ self.diff_after = self.diff_before = self._get_info(self.existing_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 == '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 == '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."""
+ # 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 did not ignore it ('create_if_not_provided'), compare SKIs
+ if ext.value.digest != csr_ext.value.digest:
+ return False
+ return True
+
+ def needs_regeneration(self, not_before=None, not_after=None):
+ """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
+
+ # Check not before
+ if not_before is not None and not self.ignore_timestamps:
+ if self.existing_certificate.not_valid_before != not_before:
+ return True
+
+ # Check not after
+ if not_after is not None and not self.ignore_timestamps:
+ if self.existing_certificate.not_valid_after != not_after:
+ return True
+ return False
+
+ def dump(self, include_certificate):
+ """Serialize the object into a dictionary."""
+ result = {
+ 'privatekey': self.privatekey_path,
+ 'csr': self.csr_path
+ }
+ # Get hold of certificate bytes
+ certificate_bytes = self.existing_certificate_bytes
+ if self.cert is not None:
+ certificate_bytes = self.get_certificate_data()
+ self.diff_after = self._get_info(certificate_bytes)
+ if include_certificate:
+ # Store result
+ result['certificate'] = certificate_bytes.decode('utf-8') if certificate_bytes else None
+
+ result['diff'] = dict(
+ before=self.diff_before,
+ after=self.diff_after,
+ )
+ 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."""
+
+ @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)
+
+ # If cryptography is available we'll use it
+ if can_use_cryptography:
+ backend = 'cryptography'
+
+ # Fail if no backend has been found
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect the required Python library "
+ "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
+
+ if 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')
+
+ 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'),
+ ignore_timestamps=dict(type='bool', default=True),
+ select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
+
+ # 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/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_acme.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_acme.py
new file mode 100644
index 00000000..18f30db5
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_acme.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import os
+import tempfile
+import traceback
+
+from ansible.module_utils.common.text.converters 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/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_entrust.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_entrust.py
new file mode 100644
index 00000000..baf53f5d
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_entrust.py
@@ -0,0 +1,211 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import datetime
+import time
+import os
+
+from ansible.module_utils.common.text.converters 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 == '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 == '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/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_info.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_info.py
new file mode 100644
index 00000000..a7beec6c
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_info.py
@@ -0,0 +1,411 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+import binascii
+import datetime
+import traceback
+
+from ansible.module_utils import six
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ load_certificate,
+ get_fingerprint_of_bytes,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
+ cryptography_decode_name,
+ cryptography_get_extensions_from_cert,
+ cryptography_oid_to_name,
+ cryptography_serial_number_of_cert,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
+ get_publickey_info,
+)
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ from cryptography import x509
+ from cryptography.hazmat.primitives import serialization
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+
+TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
+
+
+@six.add_metaclass(abc.ABCMeta)
+class CertificateInfoRetrieval(object):
+ def __init__(self, module, backend, content):
+ # content must be a bytes string
+ self.module = module
+ self.backend = backend
+ self.content = content
+
+ @abc.abstractmethod
+ def _get_der_bytes(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_signature_algorithm(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_subject_ordered(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_issuer_ordered(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_version(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_key_usage(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_extended_key_usage(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_basic_constraints(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_ocsp_must_staple(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_subject_alt_name(self):
+ pass
+
+ @abc.abstractmethod
+ def get_not_before(self):
+ pass
+
+ @abc.abstractmethod
+ def get_not_after(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_public_key_pem(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_public_key_object(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_subject_key_identifier(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_authority_key_identifier(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_serial_number(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_all_extensions(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_ocsp_uri(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_issuer_uri(self):
+ pass
+
+ def get_info(self, prefer_one_fingerprint=False):
+ result = dict()
+ self.cert = load_certificate(None, content=self.content, backend=self.backend)
+
+ result['signature_algorithm'] = self._get_signature_algorithm()
+ subject = self._get_subject_ordered()
+ issuer = self._get_issuer_ordered()
+ result['subject'] = dict()
+ for k, v in subject:
+ result['subject'][k] = v
+ result['subject_ordered'] = subject
+ result['issuer'] = dict()
+ for k, v in issuer:
+ result['issuer'][k] = v
+ result['issuer_ordered'] = issuer
+ result['version'] = self._get_version()
+ result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
+ result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
+ result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
+ result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
+ result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
+
+ not_before = self.get_not_before()
+ not_after = self.get_not_after()
+ result['not_before'] = not_before.strftime(TIMESTAMP_FORMAT)
+ result['not_after'] = not_after.strftime(TIMESTAMP_FORMAT)
+ result['expired'] = not_after < datetime.datetime.utcnow()
+
+ result['public_key'] = to_native(self._get_public_key_pem())
+
+ public_key_info = get_publickey_info(
+ self.module,
+ self.backend,
+ key=self._get_public_key_object(),
+ prefer_one_fingerprint=prefer_one_fingerprint)
+ result.update({
+ 'public_key_type': public_key_info['type'],
+ 'public_key_data': public_key_info['public_data'],
+ 'public_key_fingerprints': public_key_info['fingerprints'],
+ })
+
+ result['fingerprints'] = get_fingerprint_of_bytes(
+ self._get_der_bytes(), prefer_one=prefer_one_fingerprint)
+
+ ski = self._get_subject_key_identifier()
+ if ski is not None:
+ ski = to_native(binascii.hexlify(ski))
+ ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
+ result['subject_key_identifier'] = ski
+
+ aki, aci, acsn = self._get_authority_key_identifier()
+ if aki is not None:
+ aki = to_native(binascii.hexlify(aki))
+ aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
+ result['authority_key_identifier'] = aki
+ result['authority_cert_issuer'] = aci
+ result['authority_cert_serial_number'] = acsn
+
+ result['serial_number'] = self._get_serial_number()
+ result['extensions_by_oid'] = self._get_all_extensions()
+ result['ocsp_uri'] = self._get_ocsp_uri()
+ result['issuer_uri'] = self._get_issuer_uri()
+
+ return result
+
+
+class CertificateInfoRetrievalCryptography(CertificateInfoRetrieval):
+ """Validate the supplied cert, using the cryptography backend"""
+ def __init__(self, module, content):
+ super(CertificateInfoRetrievalCryptography, self).__init__(module, 'cryptography', content)
+ self.name_encoding = module.params.get('name_encoding', 'ignore')
+
+ def _get_der_bytes(self):
+ return self.cert.public_bytes(serialization.Encoding.DER)
+
+ def _get_signature_algorithm(self):
+ return cryptography_oid_to_name(self.cert.signature_algorithm_oid)
+
+ def _get_subject_ordered(self):
+ result = []
+ for attribute in self.cert.subject:
+ result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
+ return result
+
+ def _get_issuer_ordered(self):
+ result = []
+ for attribute in self.cert.issuer:
+ result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
+ return result
+
+ def _get_version(self):
+ if self.cert.version == x509.Version.v1:
+ return 1
+ if self.cert.version == x509.Version.v3:
+ return 3
+ return "unknown"
+
+ def _get_key_usage(self):
+ try:
+ current_key_ext = self.cert.extensions.get_extension_for_class(x509.KeyUsage)
+ current_key_usage = current_key_ext.value
+ key_usage = dict(
+ digital_signature=current_key_usage.digital_signature,
+ content_commitment=current_key_usage.content_commitment,
+ key_encipherment=current_key_usage.key_encipherment,
+ data_encipherment=current_key_usage.data_encipherment,
+ key_agreement=current_key_usage.key_agreement,
+ key_cert_sign=current_key_usage.key_cert_sign,
+ crl_sign=current_key_usage.crl_sign,
+ encipher_only=False,
+ decipher_only=False,
+ )
+ if key_usage['key_agreement']:
+ key_usage.update(dict(
+ encipher_only=current_key_usage.encipher_only,
+ decipher_only=current_key_usage.decipher_only
+ ))
+
+ key_usage_names = dict(
+ digital_signature='Digital Signature',
+ content_commitment='Non Repudiation',
+ key_encipherment='Key Encipherment',
+ data_encipherment='Data Encipherment',
+ key_agreement='Key Agreement',
+ key_cert_sign='Certificate Sign',
+ crl_sign='CRL Sign',
+ encipher_only='Encipher Only',
+ decipher_only='Decipher Only',
+ )
+ return sorted([
+ key_usage_names[name] for name, value in key_usage.items() if value
+ ]), current_key_ext.critical
+ except cryptography.x509.ExtensionNotFound:
+ return None, False
+
+ def _get_extended_key_usage(self):
+ try:
+ ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
+ return sorted([
+ cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
+ ]), ext_keyusage_ext.critical
+ except cryptography.x509.ExtensionNotFound:
+ return None, False
+
+ def _get_basic_constraints(self):
+ try:
+ ext_keyusage_ext = self.cert.extensions.get_extension_for_class(x509.BasicConstraints)
+ result = []
+ result.append('CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE'))
+ if ext_keyusage_ext.value.path_length is not None:
+ result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
+ return sorted(result), ext_keyusage_ext.critical
+ except cryptography.x509.ExtensionNotFound:
+ return None, False
+
+ def _get_ocsp_must_staple(self):
+ try:
+ try:
+ # This only works with cryptography >= 2.1
+ tlsfeature_ext = self.cert.extensions.get_extension_for_class(x509.TLSFeature)
+ value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
+ except AttributeError:
+ # Fallback for cryptography < 2.1
+ oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
+ tlsfeature_ext = self.cert.extensions.get_extension_for_oid(oid)
+ value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
+ return value, tlsfeature_ext.critical
+ except cryptography.x509.ExtensionNotFound:
+ return None, False
+
+ def _get_subject_alt_name(self):
+ try:
+ san_ext = self.cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
+ result = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in san_ext.value]
+ return result, san_ext.critical
+ except cryptography.x509.ExtensionNotFound:
+ return None, False
+
+ def get_not_before(self):
+ return self.cert.not_valid_before
+
+ def get_not_after(self):
+ return self.cert.not_valid_after
+
+ def _get_public_key_pem(self):
+ return self.cert.public_key().public_bytes(
+ serialization.Encoding.PEM,
+ serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+
+ def _get_public_key_object(self):
+ return self.cert.public_key()
+
+ def _get_subject_key_identifier(self):
+ try:
+ ext = self.cert.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
+ return ext.value.digest
+ except cryptography.x509.ExtensionNotFound:
+ return None
+
+ def _get_authority_key_identifier(self):
+ try:
+ ext = self.cert.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
+ issuer = None
+ if ext.value.authority_cert_issuer is not None:
+ issuer = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in ext.value.authority_cert_issuer]
+ return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
+ except cryptography.x509.ExtensionNotFound:
+ return None, None, None
+
+ def _get_serial_number(self):
+ return cryptography_serial_number_of_cert(self.cert)
+
+ def _get_all_extensions(self):
+ return cryptography_get_extensions_from_cert(self.cert)
+
+ def _get_ocsp_uri(self):
+ try:
+ ext = self.cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess)
+ for desc in ext.value:
+ if desc.access_method == x509.oid.AuthorityInformationAccessOID.OCSP:
+ if isinstance(desc.access_location, x509.UniformResourceIdentifier):
+ return desc.access_location.value
+ except x509.ExtensionNotFound as dummy:
+ pass
+ return None
+
+ def _get_issuer_uri(self):
+ try:
+ ext = self.cert.extensions.get_extension_for_class(x509.AuthorityInformationAccess)
+ for desc in ext.value:
+ if desc.access_method == x509.oid.AuthorityInformationAccessOID.CA_ISSUERS:
+ if isinstance(desc.access_location, x509.UniformResourceIdentifier):
+ return desc.access_location.value
+ except x509.ExtensionNotFound as dummy:
+ pass
+ return None
+
+
+def get_certificate_info(module, backend, content, prefer_one_fingerprint=False):
+ if backend == 'cryptography':
+ info = CertificateInfoRetrievalCryptography(module, content)
+ return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
+
+
+def select_backend(module, backend, content):
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
+
+ # Try cryptography
+ if can_use_cryptography:
+ backend = 'cryptography'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect any of the required Python libraries "
+ "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
+
+ if backend == 'cryptography':
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+ return backend, CertificateInfoRetrievalCryptography(module, content)
+ else:
+ raise ValueError('Unsupported value for backend: {0}'.format(backend))
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_ownca.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_ownca.py
new file mode 100644
index 00000000..ac1cf845
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_ownca.py
@@ -0,0 +1,276 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import os
+
+from random import randrange
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+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_compare_public_keys,
+ cryptography_key_needs_digest_for_signing,
+ cryptography_serial_number_of_cert,
+ cryptography_verify_certificate_signature,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
+ CRYPTOGRAPHY_VERSION,
+ CertificateError,
+ CertificateBackend,
+ CertificateProvider,
+)
+
+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 not cryptography_compare_public_keys(self.ca_cert.public_key(), self.ca_private_key.public_key()):
+ raise CertificateError('The CA private key does not belong to the CA certificate')
+
+ 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(not_before=self.notBefore, not_after=self.notAfter):
+ return True
+
+ self._ensure_existing_certificate_loaded()
+
+ # Check whether certificate is signed by CA certificate
+ if not cryptography_verify_certificate_signature(self.existing_certificate, self.ca_cert.public_key()):
+ return True
+
+ # Check subject
+ if self.ca_cert.subject != self.existing_certificate.issuer:
+ 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())
+
+ 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 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)
+
+
+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/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py
new file mode 100644
index 00000000..8695d43e
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/certificate_selfsigned.py
@@ -0,0 +1,198 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import os
+
+from random import randrange
+
+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,
+ cryptography_verify_certificate_signature,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate import (
+ CertificateError,
+ CertificateBackend,
+ CertificateProvider,
+)
+
+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 needs_regeneration(self):
+ if super(SelfSignedCertificateBackendCryptography, self).needs_regeneration(not_before=self.notBefore, not_after=self.notAfter):
+ return True
+
+ self._ensure_existing_certificate_loaded()
+
+ # Check whether certificate is signed by private key
+ if not cryptography_verify_certificate_signature(self.existing_certificate, self.privatekey.public_key()):
+ return True
+
+ return False
+
+ 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 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)
+
+
+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/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/common.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/common.py
new file mode 100644
index 00000000..67f87dd0
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/common.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+from ansible.module_utils.basic import AnsibleModule
+
+
+class ArgumentSpec:
+ def __init__(self, argument_spec, mutually_exclusive=None, required_together=None, required_one_of=None, required_if=None, required_by=None):
+ self.argument_spec = argument_spec
+ self.mutually_exclusive = mutually_exclusive or []
+ self.required_together = required_together or []
+ self.required_one_of = required_one_of or []
+ self.required_if = required_if or []
+ self.required_by = required_by or {}
+
+ def create_ansible_module_helper(self, clazz, args, **kwargs):
+ return clazz(
+ *args,
+ argument_spec=self.argument_spec,
+ mutually_exclusive=self.mutually_exclusive,
+ required_together=self.required_together,
+ required_one_of=self.required_one_of,
+ required_if=self.required_if,
+ required_by=self.required_by,
+ **kwargs)
+
+ def create_ansible_module(self, **kwargs):
+ return self.create_ansible_module_helper(AnsibleModule, (), **kwargs)
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/crl_info.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/crl_info.py
new file mode 100644
index 00000000..a5b1b8ec
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/crl_info.py
@@ -0,0 +1,102 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import traceback
+
+from ansible.module_utils.basic import missing_required_lib
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
+ cryptography_oid_to_name,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import (
+ TIMESTAMP_FORMAT,
+ cryptography_decode_revoked_certificate,
+ cryptography_dump_revoked,
+ cryptography_get_signature_algorithm_oid_from_crl,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
+ identify_pem_format,
+)
+
+# crypto_utils
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ from cryptography import x509
+ from cryptography.hazmat.backends import default_backend
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+
+class CRLInfoRetrieval(object):
+ def __init__(self, module, content, list_revoked_certificates=True):
+ # content must be a bytes string
+ self.module = module
+ self.content = content
+ self.list_revoked_certificates = list_revoked_certificates
+ self.name_encoding = module.params.get('name_encoding', 'ignore')
+
+ def get_info(self):
+ self.crl_pem = identify_pem_format(self.content)
+ try:
+ if self.crl_pem:
+ self.crl = x509.load_pem_x509_crl(self.content, default_backend())
+ else:
+ self.crl = x509.load_der_x509_crl(self.content, default_backend())
+ except ValueError as e:
+ self.module.fail_json(msg='Error while decoding CRL: {0}'.format(e))
+
+ result = {
+ 'changed': False,
+ 'format': 'pem' if self.crl_pem else 'der',
+ 'last_update': None,
+ 'next_update': None,
+ 'digest': None,
+ 'issuer_ordered': None,
+ 'issuer': None,
+ }
+
+ result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
+ result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
+ result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl))
+ issuer = []
+ for attribute in self.crl.issuer:
+ issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value])
+ result['issuer_ordered'] = issuer
+ result['issuer'] = {}
+ for k, v in issuer:
+ result['issuer'][k] = v
+ if self.list_revoked_certificates:
+ result['revoked_certificates'] = []
+ for cert in self.crl:
+ entry = cryptography_decode_revoked_certificate(cert)
+ result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding))
+
+ return result
+
+
+def get_crl_info(module, content, list_revoked_certificates=True):
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+
+ info = CRLInfoRetrieval(module, content, list_revoked_certificates=list_revoked_certificates)
+ return info.get_info()
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr.py
new file mode 100644
index 00000000..4ab14e52
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr.py
@@ -0,0 +1,675 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+import binascii
+import traceback
+
+from ansible.module_utils import six
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.common.text.converters import to_native, to_text
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+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_request,
+ parse_name_field,
+ parse_ordered_name_field,
+ select_message_digest,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
+ cryptography_get_basic_constraints,
+ cryptography_get_name,
+ cryptography_name_to_oid,
+ cryptography_key_needs_digest_for_signing,
+ cryptography_parse_key_usage_params,
+ cryptography_parse_relative_distinguished_name,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import (
+ REVOCATION_REASON_MAP,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import (
+ get_csr_info,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
+
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ import cryptography.x509
+ import cryptography.x509.oid
+ import cryptography.exceptions
+ import cryptography.hazmat.backends
+ import cryptography.hazmat.primitives.serialization
+ import cryptography.hazmat.primitives.hashes
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+ CRYPTOGRAPHY_MUST_STAPLE_NAME = cryptography.x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
+ CRYPTOGRAPHY_MUST_STAPLE_VALUE = b"\x30\x03\x02\x01\x05"
+
+
+class CertificateSigningRequestError(OpenSSLObjectError):
+ pass
+
+
+# From the object called `module`, only the following properties are used:
+#
+# - module.params[]
+# - module.warn(msg: str)
+# - module.fail_json(msg: str, **kwargs)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class CertificateSigningRequestBackend(object):
+ def __init__(self, module, backend):
+ self.module = module
+ self.backend = backend
+ self.digest = module.params['digest']
+ self.privatekey_path = module.params['privatekey_path']
+ self.privatekey_content = module.params['privatekey_content']
+ if self.privatekey_content is not None:
+ self.privatekey_content = self.privatekey_content.encode('utf-8')
+ self.privatekey_passphrase = module.params['privatekey_passphrase']
+ self.version = module.params['version']
+ self.subjectAltName = module.params['subject_alt_name']
+ self.subjectAltName_critical = module.params['subject_alt_name_critical']
+ self.keyUsage = module.params['key_usage']
+ self.keyUsage_critical = module.params['key_usage_critical']
+ self.extendedKeyUsage = module.params['extended_key_usage']
+ self.extendedKeyUsage_critical = module.params['extended_key_usage_critical']
+ self.basicConstraints = module.params['basic_constraints']
+ self.basicConstraints_critical = module.params['basic_constraints_critical']
+ self.ocspMustStaple = module.params['ocsp_must_staple']
+ self.ocspMustStaple_critical = module.params['ocsp_must_staple_critical']
+ self.name_constraints_permitted = module.params['name_constraints_permitted'] or []
+ self.name_constraints_excluded = module.params['name_constraints_excluded'] or []
+ self.name_constraints_critical = module.params['name_constraints_critical']
+ self.create_subject_key_identifier = module.params['create_subject_key_identifier']
+ self.subject_key_identifier = module.params['subject_key_identifier']
+ self.authority_key_identifier = module.params['authority_key_identifier']
+ self.authority_cert_issuer = module.params['authority_cert_issuer']
+ self.authority_cert_serial_number = module.params['authority_cert_serial_number']
+ self.crl_distribution_points = module.params['crl_distribution_points']
+ self.csr = None
+ self.privatekey = None
+
+ if self.create_subject_key_identifier and self.subject_key_identifier is not None:
+ module.fail_json(msg='subject_key_identifier cannot be specified if create_subject_key_identifier is true')
+
+ self.ordered_subject = False
+ self.subject = [
+ ('C', module.params['country_name']),
+ ('ST', module.params['state_or_province_name']),
+ ('L', module.params['locality_name']),
+ ('O', module.params['organization_name']),
+ ('OU', module.params['organizational_unit_name']),
+ ('CN', module.params['common_name']),
+ ('emailAddress', module.params['email_address']),
+ ]
+ self.subject = [(entry[0], entry[1]) for entry in self.subject if entry[1]]
+
+ try:
+ if module.params['subject']:
+ self.subject = self.subject + parse_name_field(module.params['subject'], 'subject')
+ if module.params['subject_ordered']:
+ if self.subject:
+ raise CertificateSigningRequestError('subject_ordered cannot be combined with any other subject field')
+ self.subject = parse_ordered_name_field(module.params['subject_ordered'], 'subject_ordered')
+ self.ordered_subject = True
+ except ValueError as exc:
+ raise CertificateSigningRequestError(to_native(exc))
+
+ self.using_common_name_for_san = False
+ if not self.subjectAltName and module.params['use_common_name_for_san']:
+ for sub in self.subject:
+ if sub[0] in ('commonName', 'CN'):
+ self.subjectAltName = ['DNS:%s' % sub[1]]
+ self.using_common_name_for_san = True
+ break
+
+ if self.subject_key_identifier is not None:
+ try:
+ self.subject_key_identifier = binascii.unhexlify(self.subject_key_identifier.replace(':', ''))
+ except Exception as e:
+ raise CertificateSigningRequestError('Cannot parse subject_key_identifier: {0}'.format(e))
+
+ if self.authority_key_identifier is not None:
+ try:
+ self.authority_key_identifier = binascii.unhexlify(self.authority_key_identifier.replace(':', ''))
+ except Exception as e:
+ raise CertificateSigningRequestError('Cannot parse authority_key_identifier: {0}'.format(e))
+
+ self.existing_csr = None
+ self.existing_csr_bytes = None
+
+ self.diff_before = self._get_info(None)
+ self.diff_after = self._get_info(None)
+
+ def _get_info(self, data):
+ if data is None:
+ return dict()
+ try:
+ result = get_csr_info(
+ self.module, self.backend, data, validate_signature=False, prefer_one_fingerprint=True)
+ result['can_parse_csr'] = True
+ return result
+ except Exception as exc:
+ return dict(can_parse_csr=False)
+
+ @abc.abstractmethod
+ def generate_csr(self):
+ """(Re-)Generate CSR."""
+ pass
+
+ @abc.abstractmethod
+ def get_csr_data(self):
+ """Return bytes for self.csr."""
+ pass
+
+ def set_existing(self, csr_bytes):
+ """Set existing CSR bytes. None indicates that the CSR does not exist."""
+ self.existing_csr_bytes = csr_bytes
+ self.diff_after = self.diff_before = self._get_info(self.existing_csr_bytes)
+
+ def has_existing(self):
+ """Query whether an existing CSR is/has been there."""
+ return self.existing_csr_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
+ 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 CertificateSigningRequestError(exc)
+
+ @abc.abstractmethod
+ def _check_csr(self):
+ """Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated."""
+ pass
+
+ def needs_regeneration(self):
+ """Check whether a regeneration is necessary."""
+ if self.existing_csr_bytes is None:
+ return True
+ try:
+ self.existing_csr = load_certificate_request(None, content=self.existing_csr_bytes, backend=self.backend)
+ except Exception as dummy:
+ return True
+ self._ensure_private_key_loaded()
+ return not self._check_csr()
+
+ def dump(self, include_csr):
+ """Serialize the object into a dictionary."""
+ result = {
+ 'privatekey': self.privatekey_path,
+ 'subject': self.subject,
+ 'subjectAltName': self.subjectAltName,
+ 'keyUsage': self.keyUsage,
+ 'extendedKeyUsage': self.extendedKeyUsage,
+ 'basicConstraints': self.basicConstraints,
+ 'ocspMustStaple': self.ocspMustStaple,
+ 'name_constraints_permitted': self.name_constraints_permitted,
+ 'name_constraints_excluded': self.name_constraints_excluded,
+ }
+ # Get hold of CSR bytes
+ csr_bytes = self.existing_csr_bytes
+ if self.csr is not None:
+ csr_bytes = self.get_csr_data()
+ self.diff_after = self._get_info(csr_bytes)
+ if include_csr:
+ # Store result
+ result['csr'] = csr_bytes.decode('utf-8') if csr_bytes else None
+
+ result['diff'] = dict(
+ before=self.diff_before,
+ after=self.diff_after,
+ )
+ return result
+
+
+def parse_crl_distribution_points(module, crl_distribution_points):
+ result = []
+ for index, parse_crl_distribution_point in enumerate(crl_distribution_points):
+ try:
+ params = dict(
+ full_name=None,
+ relative_name=None,
+ crl_issuer=None,
+ reasons=None,
+ )
+ if parse_crl_distribution_point['full_name'] is not None:
+ if not parse_crl_distribution_point['full_name']:
+ raise OpenSSLObjectError('full_name must not be empty')
+ params['full_name'] = [cryptography_get_name(name, 'full name') for name in parse_crl_distribution_point['full_name']]
+ if parse_crl_distribution_point['relative_name'] is not None:
+ if not parse_crl_distribution_point['relative_name']:
+ raise OpenSSLObjectError('relative_name must not be empty')
+ try:
+ params['relative_name'] = cryptography_parse_relative_distinguished_name(parse_crl_distribution_point['relative_name'])
+ except Exception:
+ # If cryptography's version is < 1.6, the error is probably caused by that
+ if CRYPTOGRAPHY_VERSION < LooseVersion('1.6'):
+ raise OpenSSLObjectError('Cannot specify relative_name for cryptography < 1.6')
+ raise
+ if parse_crl_distribution_point['crl_issuer'] is not None:
+ if not parse_crl_distribution_point['crl_issuer']:
+ raise OpenSSLObjectError('crl_issuer must not be empty')
+ params['crl_issuer'] = [cryptography_get_name(name, 'CRL issuer') for name in parse_crl_distribution_point['crl_issuer']]
+ if parse_crl_distribution_point['reasons'] is not None:
+ reasons = []
+ for reason in parse_crl_distribution_point['reasons']:
+ reasons.append(REVOCATION_REASON_MAP[reason])
+ params['reasons'] = frozenset(reasons)
+ result.append(cryptography.x509.DistributionPoint(**params))
+ except (OpenSSLObjectError, ValueError) as e:
+ raise OpenSSLObjectError('Error while parsing CRL distribution point #{index}: {error}'.format(index=index, error=e))
+ return result
+
+
+# Implementation with using cryptography
+class CertificateSigningRequestCryptographyBackend(CertificateSigningRequestBackend):
+ def __init__(self, module):
+ super(CertificateSigningRequestCryptographyBackend, self).__init__(module, 'cryptography')
+ self.cryptography_backend = cryptography.hazmat.backends.default_backend()
+ if self.version != 1:
+ module.warn('The cryptography backend only supports version 1. (The only valid value according to RFC 2986.)')
+
+ if self.crl_distribution_points:
+ self.crl_distribution_points = parse_crl_distribution_points(module, self.crl_distribution_points)
+
+ def generate_csr(self):
+ """(Re-)Generate CSR."""
+ self._ensure_private_key_loaded()
+
+ csr = cryptography.x509.CertificateSigningRequestBuilder()
+ try:
+ csr = csr.subject_name(cryptography.x509.Name([
+ cryptography.x509.NameAttribute(cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject
+ ]))
+ except ValueError as e:
+ raise CertificateSigningRequestError(e)
+
+ if self.subjectAltName:
+ csr = csr.add_extension(cryptography.x509.SubjectAlternativeName([
+ cryptography_get_name(name) for name in self.subjectAltName
+ ]), critical=self.subjectAltName_critical)
+
+ if self.keyUsage:
+ params = cryptography_parse_key_usage_params(self.keyUsage)
+ csr = csr.add_extension(cryptography.x509.KeyUsage(**params), critical=self.keyUsage_critical)
+
+ if self.extendedKeyUsage:
+ usages = [cryptography_name_to_oid(usage) for usage in self.extendedKeyUsage]
+ csr = csr.add_extension(cryptography.x509.ExtendedKeyUsage(usages), critical=self.extendedKeyUsage_critical)
+
+ if self.basicConstraints:
+ params = {}
+ ca, path_length = cryptography_get_basic_constraints(self.basicConstraints)
+ csr = csr.add_extension(cryptography.x509.BasicConstraints(ca, path_length), critical=self.basicConstraints_critical)
+
+ if self.ocspMustStaple:
+ try:
+ # This only works with cryptography >= 2.1
+ csr = csr.add_extension(cryptography.x509.TLSFeature([cryptography.x509.TLSFeatureType.status_request]), critical=self.ocspMustStaple_critical)
+ except AttributeError as dummy:
+ csr = csr.add_extension(
+ cryptography.x509.UnrecognizedExtension(CRYPTOGRAPHY_MUST_STAPLE_NAME, CRYPTOGRAPHY_MUST_STAPLE_VALUE),
+ critical=self.ocspMustStaple_critical
+ )
+
+ if self.name_constraints_permitted or self.name_constraints_excluded:
+ try:
+ csr = csr.add_extension(cryptography.x509.NameConstraints(
+ [cryptography_get_name(name, 'name constraints permitted') for name in self.name_constraints_permitted] or None,
+ [cryptography_get_name(name, 'name constraints excluded') for name in self.name_constraints_excluded] or None,
+ ), critical=self.name_constraints_critical)
+ except TypeError as e:
+ raise OpenSSLObjectError('Error while parsing name constraint: {0}'.format(e))
+
+ if self.create_subject_key_identifier:
+ csr = csr.add_extension(
+ cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()),
+ critical=False
+ )
+ elif self.subject_key_identifier is not None:
+ csr = csr.add_extension(cryptography.x509.SubjectKeyIdentifier(self.subject_key_identifier), critical=False)
+
+ if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None:
+ issuers = None
+ if self.authority_cert_issuer is not None:
+ issuers = [cryptography_get_name(n, 'authority cert issuer') for n in self.authority_cert_issuer]
+ csr = csr.add_extension(
+ cryptography.x509.AuthorityKeyIdentifier(self.authority_key_identifier, issuers, self.authority_cert_serial_number),
+ critical=False
+ )
+
+ if self.crl_distribution_points:
+ csr = csr.add_extension(
+ cryptography.x509.CRLDistributionPoints(self.crl_distribution_points),
+ critical=False
+ )
+
+ digest = None
+ if cryptography_key_needs_digest_for_signing(self.privatekey):
+ digest = select_message_digest(self.digest)
+ if digest is None:
+ raise CertificateSigningRequestError('Unsupported digest "{0}"'.format(self.digest))
+ try:
+ self.csr = csr.sign(self.privatekey, digest, self.cryptography_backend)
+ except TypeError as e:
+ if str(e) == 'Algorithm must be a registered hash algorithm.' and digest is None:
+ self.module.fail_json(msg='Signing with Ed25519 and Ed448 keys requires cryptography 2.8 or newer.')
+ raise
+ except UnicodeError as e:
+ # This catches IDNAErrors, which happens when a bad name is passed as a SAN
+ # (https://github.com/ansible-collections/community.crypto/issues/105).
+ # For older cryptography versions, this is handled by idna, which raises
+ # an idna.core.IDNAError. Later versions of cryptography deprecated and stopped
+ # requiring idna, whence we cannot easily handle this error. Fortunately, in
+ # most versions of idna, IDNAError extends UnicodeError. There is only version
+ # 2.3 where it extends Exception instead (see
+ # https://github.com/kjd/idna/commit/ebefacd3134d0f5da4745878620a6a1cba86d130
+ # and then
+ # https://github.com/kjd/idna/commit/ea03c7b5db7d2a99af082e0239da2b68aeea702a).
+ msg = 'Error while creating CSR: {0}\n'.format(e)
+ if self.using_common_name_for_san:
+ self.module.fail_json(msg=msg + 'This is probably caused because the Common Name is used as a SAN.'
+ ' Specifying use_common_name_for_san=false might fix this.')
+ self.module.fail_json(msg=msg + 'This is probably caused by an invalid Subject Alternative DNS Name.')
+
+ def get_csr_data(self):
+ """Return bytes for self.csr."""
+ return self.csr.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM)
+
+ def _check_csr(self):
+ """Check whether provided parameters, assuming self.existing_csr and self.privatekey have been populated."""
+ def _check_subject(csr):
+ subject = [(cryptography_name_to_oid(entry[0]), to_text(entry[1])) for entry in self.subject]
+ current_subject = [(sub.oid, sub.value) for sub in csr.subject]
+ if self.ordered_subject:
+ return subject == current_subject
+ else:
+ return set(subject) == set(current_subject)
+
+ def _find_extension(extensions, exttype):
+ return next(
+ (ext for ext in extensions if isinstance(ext.value, exttype)),
+ None
+ )
+
+ def _check_subjectAltName(extensions):
+ current_altnames_ext = _find_extension(extensions, cryptography.x509.SubjectAlternativeName)
+ current_altnames = [to_text(altname) for altname in current_altnames_ext.value] if current_altnames_ext else []
+ altnames = [to_text(cryptography_get_name(altname)) for altname in self.subjectAltName] if self.subjectAltName else []
+ if set(altnames) != set(current_altnames):
+ return False
+ if altnames:
+ if current_altnames_ext.critical != self.subjectAltName_critical:
+ return False
+ return True
+
+ def _check_keyUsage(extensions):
+ current_keyusage_ext = _find_extension(extensions, cryptography.x509.KeyUsage)
+ if not self.keyUsage:
+ return current_keyusage_ext is None
+ elif current_keyusage_ext is None:
+ return False
+ params = cryptography_parse_key_usage_params(self.keyUsage)
+ for param in params:
+ if getattr(current_keyusage_ext.value, '_' + param) != params[param]:
+ return False
+ if current_keyusage_ext.critical != self.keyUsage_critical:
+ return False
+ return True
+
+ def _check_extenededKeyUsage(extensions):
+ current_usages_ext = _find_extension(extensions, cryptography.x509.ExtendedKeyUsage)
+ current_usages = [str(usage) for usage in current_usages_ext.value] if current_usages_ext else []
+ usages = [str(cryptography_name_to_oid(usage)) for usage in self.extendedKeyUsage] if self.extendedKeyUsage else []
+ if set(current_usages) != set(usages):
+ return False
+ if usages:
+ if current_usages_ext.critical != self.extendedKeyUsage_critical:
+ return False
+ return True
+
+ def _check_basicConstraints(extensions):
+ bc_ext = _find_extension(extensions, cryptography.x509.BasicConstraints)
+ current_ca = bc_ext.value.ca if bc_ext else False
+ current_path_length = bc_ext.value.path_length if bc_ext else None
+ ca, path_length = cryptography_get_basic_constraints(self.basicConstraints)
+ # Check CA flag
+ if ca != current_ca:
+ return False
+ # Check path length
+ if path_length != current_path_length:
+ return False
+ # Check criticality
+ if self.basicConstraints:
+ return bc_ext is not None and bc_ext.critical == self.basicConstraints_critical
+ else:
+ return bc_ext is None
+
+ def _check_ocspMustStaple(extensions):
+ try:
+ # This only works with cryptography >= 2.1
+ tlsfeature_ext = _find_extension(extensions, cryptography.x509.TLSFeature)
+ has_tlsfeature = True
+ except AttributeError as dummy:
+ tlsfeature_ext = next(
+ (ext for ext in extensions if ext.value.oid == CRYPTOGRAPHY_MUST_STAPLE_NAME),
+ None
+ )
+ has_tlsfeature = False
+ if self.ocspMustStaple:
+ if not tlsfeature_ext or tlsfeature_ext.critical != self.ocspMustStaple_critical:
+ return False
+ if has_tlsfeature:
+ return cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
+ else:
+ return tlsfeature_ext.value.value == CRYPTOGRAPHY_MUST_STAPLE_VALUE
+ else:
+ return tlsfeature_ext is None
+
+ def _check_nameConstraints(extensions):
+ current_nc_ext = _find_extension(extensions, cryptography.x509.NameConstraints)
+ current_nc_perm = [to_text(altname) for altname in current_nc_ext.value.permitted_subtrees or []] if current_nc_ext else []
+ current_nc_excl = [to_text(altname) for altname in current_nc_ext.value.excluded_subtrees or []] if current_nc_ext else []
+ nc_perm = [to_text(cryptography_get_name(altname, 'name constraints permitted')) for altname in self.name_constraints_permitted]
+ nc_excl = [to_text(cryptography_get_name(altname, 'name constraints excluded')) for altname in self.name_constraints_excluded]
+ if set(nc_perm) != set(current_nc_perm) or set(nc_excl) != set(current_nc_excl):
+ return False
+ if nc_perm or nc_excl:
+ if current_nc_ext.critical != self.name_constraints_critical:
+ return False
+ return True
+
+ def _check_subject_key_identifier(extensions):
+ ext = _find_extension(extensions, cryptography.x509.SubjectKeyIdentifier)
+ if self.create_subject_key_identifier or self.subject_key_identifier is not None:
+ if not ext or ext.critical:
+ return False
+ if self.create_subject_key_identifier:
+ digest = cryptography.x509.SubjectKeyIdentifier.from_public_key(self.privatekey.public_key()).digest
+ return ext.value.digest == digest
+ else:
+ return ext.value.digest == self.subject_key_identifier
+ else:
+ return ext is None
+
+ def _check_authority_key_identifier(extensions):
+ ext = _find_extension(extensions, cryptography.x509.AuthorityKeyIdentifier)
+ if self.authority_key_identifier is not None or self.authority_cert_issuer is not None or self.authority_cert_serial_number is not None:
+ if not ext or ext.critical:
+ return False
+ aci = None
+ csr_aci = None
+ if self.authority_cert_issuer is not None:
+ aci = [to_text(cryptography_get_name(n, 'authority cert issuer')) for n in self.authority_cert_issuer]
+ if ext.value.authority_cert_issuer is not None:
+ csr_aci = [to_text(n) for n in ext.value.authority_cert_issuer]
+ return (ext.value.key_identifier == self.authority_key_identifier
+ and csr_aci == aci
+ and ext.value.authority_cert_serial_number == self.authority_cert_serial_number)
+ else:
+ return ext is None
+
+ def _check_crl_distribution_points(extensions):
+ ext = _find_extension(extensions, cryptography.x509.CRLDistributionPoints)
+ if self.crl_distribution_points is None:
+ return ext is None
+ if not ext:
+ return False
+ return list(ext.value) == self.crl_distribution_points
+
+ def _check_extensions(csr):
+ extensions = csr.extensions
+ return (_check_subjectAltName(extensions) and _check_keyUsage(extensions) and
+ _check_extenededKeyUsage(extensions) and _check_basicConstraints(extensions) and
+ _check_ocspMustStaple(extensions) and _check_subject_key_identifier(extensions) and
+ _check_authority_key_identifier(extensions) and _check_nameConstraints(extensions) and
+ _check_crl_distribution_points(extensions))
+
+ def _check_signature(csr):
+ if not csr.is_signature_valid:
+ return False
+ # To check whether public key of CSR belongs to private key,
+ # encode both public keys and compare PEMs.
+ key_a = csr.public_key().public_bytes(
+ cryptography.hazmat.primitives.serialization.Encoding.PEM,
+ cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo
+ )
+ key_b = self.privatekey.public_key().public_bytes(
+ cryptography.hazmat.primitives.serialization.Encoding.PEM,
+ cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo
+ )
+ return key_a == key_b
+
+ return _check_subject(self.existing_csr) and _check_extensions(self.existing_csr) and _check_signature(self.existing_csr)
+
+
+def select_backend(module, backend):
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
+
+ # Try cryptography
+ if can_use_cryptography:
+ backend = 'cryptography'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect any of the required Python libraries "
+ "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
+
+ if backend == 'cryptography':
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+ return backend, CertificateSigningRequestCryptographyBackend(module)
+ else:
+ raise Exception('Unsupported value for backend: {0}'.format(backend))
+
+
+def get_csr_argument_spec():
+ return ArgumentSpec(
+ argument_spec=dict(
+ digest=dict(type='str', default='sha256'),
+ privatekey_path=dict(type='path'),
+ privatekey_content=dict(type='str', no_log=True),
+ privatekey_passphrase=dict(type='str', no_log=True),
+ version=dict(type='int', default=1, choices=[1]),
+ subject=dict(type='dict'),
+ subject_ordered=dict(type='list', elements='dict'),
+ country_name=dict(type='str', aliases=['C', 'countryName']),
+ state_or_province_name=dict(type='str', aliases=['ST', 'stateOrProvinceName']),
+ locality_name=dict(type='str', aliases=['L', 'localityName']),
+ organization_name=dict(type='str', aliases=['O', 'organizationName']),
+ organizational_unit_name=dict(type='str', aliases=['OU', 'organizationalUnitName']),
+ common_name=dict(type='str', aliases=['CN', 'commonName']),
+ email_address=dict(type='str', aliases=['E', 'emailAddress']),
+ subject_alt_name=dict(type='list', elements='str', aliases=['subjectAltName']),
+ subject_alt_name_critical=dict(type='bool', default=False, aliases=['subjectAltName_critical']),
+ use_common_name_for_san=dict(type='bool', default=True, aliases=['useCommonNameForSAN']),
+ key_usage=dict(type='list', elements='str', aliases=['keyUsage']),
+ key_usage_critical=dict(type='bool', default=False, aliases=['keyUsage_critical']),
+ extended_key_usage=dict(type='list', elements='str', aliases=['extKeyUsage', 'extendedKeyUsage']),
+ extended_key_usage_critical=dict(type='bool', default=False, aliases=['extKeyUsage_critical', 'extendedKeyUsage_critical']),
+ basic_constraints=dict(type='list', elements='str', aliases=['basicConstraints']),
+ basic_constraints_critical=dict(type='bool', default=False, aliases=['basicConstraints_critical']),
+ ocsp_must_staple=dict(type='bool', default=False, aliases=['ocspMustStaple']),
+ ocsp_must_staple_critical=dict(type='bool', default=False, aliases=['ocspMustStaple_critical']),
+ name_constraints_permitted=dict(type='list', elements='str'),
+ name_constraints_excluded=dict(type='list', elements='str'),
+ name_constraints_critical=dict(type='bool', default=False),
+ create_subject_key_identifier=dict(type='bool', default=False),
+ subject_key_identifier=dict(type='str'),
+ authority_key_identifier=dict(type='str'),
+ authority_cert_issuer=dict(type='list', elements='str'),
+ authority_cert_serial_number=dict(type='int'),
+ crl_distribution_points=dict(
+ type='list',
+ elements='dict',
+ options=dict(
+ full_name=dict(type='list', elements='str'),
+ relative_name=dict(type='list', elements='str'),
+ crl_issuer=dict(type='list', elements='str'),
+ reasons=dict(type='list', elements='str', choices=[
+ 'key_compromise',
+ 'ca_compromise',
+ 'affiliation_changed',
+ 'superseded',
+ 'cessation_of_operation',
+ 'certificate_hold',
+ 'privilege_withdrawn',
+ 'aa_compromise',
+ ]),
+ ),
+ mutually_exclusive=[('full_name', 'relative_name')],
+ required_one_of=[('full_name', 'relative_name', 'crl_issuer')],
+ ),
+ select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
+ ),
+ required_together=[
+ ['authority_cert_issuer', 'authority_cert_serial_number'],
+ ],
+ mutually_exclusive=[
+ ['privatekey_path', 'privatekey_content'],
+ ['subject', 'subject_ordered'],
+ ],
+ required_one_of=[
+ ['privatekey_path', 'privatekey_content'],
+ ],
+ )
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr_info.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr_info.py
new file mode 100644
index 00000000..fc3d0d3d
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/csr_info.py
@@ -0,0 +1,334 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+import binascii
+import traceback
+
+from ansible.module_utils import six
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ load_certificate_request,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
+ cryptography_decode_name,
+ cryptography_get_extensions_from_csr,
+ cryptography_oid_to_name,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
+ get_publickey_info,
+)
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.3'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ from cryptography import x509
+ from cryptography.hazmat.primitives import serialization
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+
+TIMESTAMP_FORMAT = "%Y%m%d%H%M%SZ"
+
+
+@six.add_metaclass(abc.ABCMeta)
+class CSRInfoRetrieval(object):
+ def __init__(self, module, backend, content, validate_signature):
+ # content must be a bytes string
+ self.module = module
+ self.backend = backend
+ self.content = content
+ self.validate_signature = validate_signature
+
+ @abc.abstractmethod
+ def _get_subject_ordered(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_key_usage(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_extended_key_usage(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_basic_constraints(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_ocsp_must_staple(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_subject_alt_name(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_name_constraints(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_public_key_pem(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_public_key_object(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_subject_key_identifier(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_authority_key_identifier(self):
+ pass
+
+ @abc.abstractmethod
+ def _get_all_extensions(self):
+ pass
+
+ @abc.abstractmethod
+ def _is_signature_valid(self):
+ pass
+
+ def get_info(self, prefer_one_fingerprint=False):
+ result = dict()
+ self.csr = load_certificate_request(None, content=self.content, backend=self.backend)
+
+ subject = self._get_subject_ordered()
+ result['subject'] = dict()
+ for k, v in subject:
+ result['subject'][k] = v
+ result['subject_ordered'] = subject
+ result['key_usage'], result['key_usage_critical'] = self._get_key_usage()
+ result['extended_key_usage'], result['extended_key_usage_critical'] = self._get_extended_key_usage()
+ result['basic_constraints'], result['basic_constraints_critical'] = self._get_basic_constraints()
+ result['ocsp_must_staple'], result['ocsp_must_staple_critical'] = self._get_ocsp_must_staple()
+ result['subject_alt_name'], result['subject_alt_name_critical'] = self._get_subject_alt_name()
+ (
+ result['name_constraints_permitted'],
+ result['name_constraints_excluded'],
+ result['name_constraints_critical'],
+ ) = self._get_name_constraints()
+
+ result['public_key'] = to_native(self._get_public_key_pem())
+
+ public_key_info = get_publickey_info(
+ self.module,
+ self.backend,
+ key=self._get_public_key_object(),
+ prefer_one_fingerprint=prefer_one_fingerprint)
+ result.update({
+ 'public_key_type': public_key_info['type'],
+ 'public_key_data': public_key_info['public_data'],
+ 'public_key_fingerprints': public_key_info['fingerprints'],
+ })
+
+ ski = self._get_subject_key_identifier()
+ if ski is not None:
+ ski = to_native(binascii.hexlify(ski))
+ ski = ':'.join([ski[i:i + 2] for i in range(0, len(ski), 2)])
+ result['subject_key_identifier'] = ski
+
+ aki, aci, acsn = self._get_authority_key_identifier()
+ if aki is not None:
+ aki = to_native(binascii.hexlify(aki))
+ aki = ':'.join([aki[i:i + 2] for i in range(0, len(aki), 2)])
+ result['authority_key_identifier'] = aki
+ result['authority_cert_issuer'] = aci
+ result['authority_cert_serial_number'] = acsn
+
+ result['extensions_by_oid'] = self._get_all_extensions()
+
+ result['signature_valid'] = self._is_signature_valid()
+ if self.validate_signature and not result['signature_valid']:
+ self.module.fail_json(
+ msg='CSR signature is invalid!',
+ **result
+ )
+ return result
+
+
+class CSRInfoRetrievalCryptography(CSRInfoRetrieval):
+ """Validate the supplied CSR, using the cryptography backend"""
+ def __init__(self, module, content, validate_signature):
+ super(CSRInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, validate_signature)
+ self.name_encoding = module.params.get('name_encoding', 'ignore')
+
+ def _get_subject_ordered(self):
+ result = []
+ for attribute in self.csr.subject:
+ result.append([cryptography_oid_to_name(attribute.oid), attribute.value])
+ return result
+
+ def _get_key_usage(self):
+ try:
+ current_key_ext = self.csr.extensions.get_extension_for_class(x509.KeyUsage)
+ current_key_usage = current_key_ext.value
+ key_usage = dict(
+ digital_signature=current_key_usage.digital_signature,
+ content_commitment=current_key_usage.content_commitment,
+ key_encipherment=current_key_usage.key_encipherment,
+ data_encipherment=current_key_usage.data_encipherment,
+ key_agreement=current_key_usage.key_agreement,
+ key_cert_sign=current_key_usage.key_cert_sign,
+ crl_sign=current_key_usage.crl_sign,
+ encipher_only=False,
+ decipher_only=False,
+ )
+ if key_usage['key_agreement']:
+ key_usage.update(dict(
+ encipher_only=current_key_usage.encipher_only,
+ decipher_only=current_key_usage.decipher_only
+ ))
+
+ key_usage_names = dict(
+ digital_signature='Digital Signature',
+ content_commitment='Non Repudiation',
+ key_encipherment='Key Encipherment',
+ data_encipherment='Data Encipherment',
+ key_agreement='Key Agreement',
+ key_cert_sign='Certificate Sign',
+ crl_sign='CRL Sign',
+ encipher_only='Encipher Only',
+ decipher_only='Decipher Only',
+ )
+ return sorted([
+ key_usage_names[name] for name, value in key_usage.items() if value
+ ]), current_key_ext.critical
+ except cryptography.x509.ExtensionNotFound:
+ return None, False
+
+ def _get_extended_key_usage(self):
+ try:
+ ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
+ return sorted([
+ cryptography_oid_to_name(eku) for eku in ext_keyusage_ext.value
+ ]), ext_keyusage_ext.critical
+ except cryptography.x509.ExtensionNotFound:
+ return None, False
+
+ def _get_basic_constraints(self):
+ try:
+ ext_keyusage_ext = self.csr.extensions.get_extension_for_class(x509.BasicConstraints)
+ result = ['CA:{0}'.format('TRUE' if ext_keyusage_ext.value.ca else 'FALSE')]
+ if ext_keyusage_ext.value.path_length is not None:
+ result.append('pathlen:{0}'.format(ext_keyusage_ext.value.path_length))
+ return sorted(result), ext_keyusage_ext.critical
+ except cryptography.x509.ExtensionNotFound:
+ return None, False
+
+ def _get_ocsp_must_staple(self):
+ try:
+ try:
+ # This only works with cryptography >= 2.1
+ tlsfeature_ext = self.csr.extensions.get_extension_for_class(x509.TLSFeature)
+ value = cryptography.x509.TLSFeatureType.status_request in tlsfeature_ext.value
+ except AttributeError:
+ # Fallback for cryptography < 2.1
+ oid = x509.oid.ObjectIdentifier("1.3.6.1.5.5.7.1.24")
+ tlsfeature_ext = self.csr.extensions.get_extension_for_oid(oid)
+ value = tlsfeature_ext.value.value == b"\x30\x03\x02\x01\x05"
+ return value, tlsfeature_ext.critical
+ except cryptography.x509.ExtensionNotFound:
+ return None, False
+
+ def _get_subject_alt_name(self):
+ try:
+ san_ext = self.csr.extensions.get_extension_for_class(x509.SubjectAlternativeName)
+ result = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in san_ext.value]
+ return result, san_ext.critical
+ except cryptography.x509.ExtensionNotFound:
+ return None, False
+
+ def _get_name_constraints(self):
+ try:
+ nc_ext = self.csr.extensions.get_extension_for_class(x509.NameConstraints)
+ permitted = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in nc_ext.value.permitted_subtrees or []]
+ excluded = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in nc_ext.value.excluded_subtrees or []]
+ return permitted, excluded, nc_ext.critical
+ except cryptography.x509.ExtensionNotFound:
+ return None, None, False
+
+ def _get_public_key_pem(self):
+ return self.csr.public_key().public_bytes(
+ serialization.Encoding.PEM,
+ serialization.PublicFormat.SubjectPublicKeyInfo,
+ )
+
+ def _get_public_key_object(self):
+ return self.csr.public_key()
+
+ def _get_subject_key_identifier(self):
+ try:
+ ext = self.csr.extensions.get_extension_for_class(x509.SubjectKeyIdentifier)
+ return ext.value.digest
+ except cryptography.x509.ExtensionNotFound:
+ return None
+
+ def _get_authority_key_identifier(self):
+ try:
+ ext = self.csr.extensions.get_extension_for_class(x509.AuthorityKeyIdentifier)
+ issuer = None
+ if ext.value.authority_cert_issuer is not None:
+ issuer = [cryptography_decode_name(san, idn_rewrite=self.name_encoding) for san in ext.value.authority_cert_issuer]
+ return ext.value.key_identifier, issuer, ext.value.authority_cert_serial_number
+ except cryptography.x509.ExtensionNotFound:
+ return None, None, None
+
+ def _get_all_extensions(self):
+ return cryptography_get_extensions_from_csr(self.csr)
+
+ def _is_signature_valid(self):
+ return self.csr.is_signature_valid
+
+
+def get_csr_info(module, backend, content, validate_signature=True, prefer_one_fingerprint=False):
+ if backend == 'cryptography':
+ info = CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature)
+ return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
+
+
+def select_backend(module, backend, content, validate_signature=True):
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
+
+ # Try cryptography
+ if can_use_cryptography:
+ backend = 'cryptography'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect the required Python library "
+ "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
+
+ if backend == 'cryptography':
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+ return backend, CSRInfoRetrievalCryptography(module, content, validate_signature=validate_signature)
+ else:
+ raise ValueError('Unsupported value for backend: {0}'.format(backend))
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey.py
new file mode 100644
index 00000000..dc13107b
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey.py
@@ -0,0 +1,533 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+import base64
+import traceback
+
+from ansible.module_utils import six
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.common.text.converters import to_bytes
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ CRYPTOGRAPHY_HAS_X25519,
+ CRYPTOGRAPHY_HAS_X25519_FULL,
+ CRYPTOGRAPHY_HAS_X448,
+ CRYPTOGRAPHY_HAS_ED25519,
+ CRYPTOGRAPHY_HAS_ED448,
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ get_fingerprint_of_privatekey,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
+ identify_private_key_format,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import (
+ PrivateKeyConsistencyError,
+ PrivateKeyParseError,
+ get_privatekey_info,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
+
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ import cryptography.exceptions
+ import cryptography.hazmat.backends
+ import cryptography.hazmat.primitives.serialization
+ import cryptography.hazmat.primitives.asymmetric.rsa
+ import cryptography.hazmat.primitives.asymmetric.dsa
+ import cryptography.hazmat.primitives.asymmetric.ec
+ import cryptography.hazmat.primitives.asymmetric.utils
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+
+class PrivateKeyError(OpenSSLObjectError):
+ pass
+
+
+# From the object called `module`, only the following properties are used:
+#
+# - module.params[]
+# - module.warn(msg: str)
+# - module.fail_json(msg: str, **kwargs)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class PrivateKeyBackend:
+ def __init__(self, module, backend):
+ self.module = module
+ self.type = module.params['type']
+ self.size = module.params['size']
+ self.curve = module.params['curve']
+ self.passphrase = module.params['passphrase']
+ self.cipher = module.params['cipher']
+ self.format = module.params['format']
+ self.format_mismatch = module.params.get('format_mismatch', 'regenerate')
+ self.regenerate = module.params.get('regenerate', 'full_idempotence')
+ self.backend = backend
+
+ self.private_key = None
+
+ self.existing_private_key = None
+ self.existing_private_key_bytes = None
+
+ self.diff_before = self._get_info(None)
+ self.diff_after = self._get_info(None)
+
+ def _get_info(self, data):
+ if data is None:
+ return dict()
+ result = dict(can_parse_key=False)
+ try:
+ result.update(get_privatekey_info(
+ self.module, self.backend, data, passphrase=self.passphrase,
+ return_private_key_data=False, prefer_one_fingerprint=True))
+ except PrivateKeyConsistencyError as exc:
+ result.update(exc.result)
+ except PrivateKeyParseError as exc:
+ result.update(exc.result)
+ except Exception as exc:
+ pass
+ return result
+
+ @abc.abstractmethod
+ def generate_private_key(self):
+ """(Re-)Generate private key."""
+ pass
+
+ def convert_private_key(self):
+ """Convert existing private key (self.existing_private_key) to new private key (self.private_key).
+
+ This is effectively a copy without active conversion. The conversion is done
+ during load and store; get_private_key_data() uses the destination format to
+ serialize the key.
+ """
+ self._ensure_existing_private_key_loaded()
+ self.private_key = self.existing_private_key
+
+ @abc.abstractmethod
+ def get_private_key_data(self):
+ """Return bytes for self.private_key."""
+ pass
+
+ def set_existing(self, privatekey_bytes):
+ """Set existing private key bytes. None indicates that the key does not exist."""
+ self.existing_private_key_bytes = privatekey_bytes
+ self.diff_after = self.diff_before = self._get_info(self.existing_private_key_bytes)
+
+ def has_existing(self):
+ """Query whether an existing private key is/has been there."""
+ return self.existing_private_key_bytes is not None
+
+ @abc.abstractmethod
+ def _check_passphrase(self):
+ """Check whether provided passphrase matches, assuming self.existing_private_key_bytes has been populated."""
+ pass
+
+ @abc.abstractmethod
+ def _ensure_existing_private_key_loaded(self):
+ """Make sure that self.existing_private_key is populated from self.existing_private_key_bytes."""
+ pass
+
+ @abc.abstractmethod
+ def _check_size_and_type(self):
+ """Check whether provided size and type matches, assuming self.existing_private_key has been populated."""
+ pass
+
+ @abc.abstractmethod
+ def _check_format(self):
+ """Check whether the key file format, assuming self.existing_private_key and self.existing_private_key_bytes has been populated."""
+ pass
+
+ def needs_regeneration(self):
+ """Check whether a regeneration is necessary."""
+ if self.regenerate == 'always':
+ return True
+ if not self.has_existing():
+ # key does not exist
+ return True
+ if not self._check_passphrase():
+ if self.regenerate == 'full_idempotence':
+ return True
+ self.module.fail_json(msg='Unable to read the key. The key is protected with a another passphrase / no passphrase or broken.'
+ ' Will not proceed. To force regeneration, call the module with `generate`'
+ ' set to `full_idempotence` or `always`, or with `force=true`.')
+ self._ensure_existing_private_key_loaded()
+ if self.regenerate != 'never':
+ if not self._check_size_and_type():
+ if self.regenerate in ('partial_idempotence', 'full_idempotence'):
+ return True
+ self.module.fail_json(msg='Key has wrong type and/or size.'
+ ' Will not proceed. To force regeneration, call the module with `generate`'
+ ' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=true`.')
+ # During generation step, regenerate if format does not match and format_mismatch == 'regenerate'
+ if self.format_mismatch == 'regenerate' and self.regenerate != 'never':
+ if not self._check_format():
+ if self.regenerate in ('partial_idempotence', 'full_idempotence'):
+ return True
+ self.module.fail_json(msg='Key has wrong format.'
+ ' Will not proceed. To force regeneration, call the module with `generate`'
+ ' set to `partial_idempotence`, `full_idempotence` or `always`, or with `force=true`.'
+ ' To convert the key, set `format_mismatch` to `convert`.')
+ return False
+
+ def needs_conversion(self):
+ """Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False."""
+ # During conversion step, convert if format does not match and format_mismatch == 'convert'
+ self._ensure_existing_private_key_loaded()
+ return self.has_existing() and self.format_mismatch == 'convert' and not self._check_format()
+
+ def _get_fingerprint(self):
+ if self.private_key:
+ return get_fingerprint_of_privatekey(self.private_key, backend=self.backend)
+ try:
+ self._ensure_existing_private_key_loaded()
+ except Exception as dummy:
+ # Ignore errors
+ pass
+ if self.existing_private_key:
+ return get_fingerprint_of_privatekey(self.existing_private_key, backend=self.backend)
+
+ def dump(self, include_key):
+ """Serialize the object into a dictionary."""
+
+ if not self.private_key:
+ try:
+ self._ensure_existing_private_key_loaded()
+ except Exception as dummy:
+ # Ignore errors
+ pass
+ result = {
+ 'type': self.type,
+ 'size': self.size,
+ 'fingerprint': self._get_fingerprint(),
+ }
+ if self.type == 'ECC':
+ result['curve'] = self.curve
+ # Get hold of private key bytes
+ pk_bytes = self.existing_private_key_bytes
+ if self.private_key is not None:
+ pk_bytes = self.get_private_key_data()
+ self.diff_after = self._get_info(pk_bytes)
+ if include_key:
+ # Store result
+ if pk_bytes:
+ if identify_private_key_format(pk_bytes) == 'raw':
+ result['privatekey'] = base64.b64encode(pk_bytes)
+ else:
+ result['privatekey'] = pk_bytes.decode('utf-8')
+ else:
+ result['privatekey'] = None
+
+ result['diff'] = dict(
+ before=self.diff_before,
+ after=self.diff_after,
+ )
+ return result
+
+
+# Implementation with using cryptography
+class PrivateKeyCryptographyBackend(PrivateKeyBackend):
+
+ def _get_ec_class(self, ectype):
+ ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(ectype)
+ if ecclass is None:
+ self.module.fail_json(msg='Your cryptography version does not support {0}'.format(ectype))
+ return ecclass
+
+ def _add_curve(self, name, ectype, deprecated=False):
+ def create(size):
+ ecclass = self._get_ec_class(ectype)
+ return ecclass()
+
+ def verify(privatekey):
+ ecclass = self._get_ec_class(ectype)
+ return isinstance(privatekey.private_numbers().public_numbers.curve, ecclass)
+
+ self.curves[name] = {
+ 'create': create,
+ 'verify': verify,
+ 'deprecated': deprecated,
+ }
+
+ def __init__(self, module):
+ super(PrivateKeyCryptographyBackend, self).__init__(module=module, backend='cryptography')
+
+ self.curves = dict()
+ self._add_curve('secp224r1', 'SECP224R1')
+ self._add_curve('secp256k1', 'SECP256K1')
+ self._add_curve('secp256r1', 'SECP256R1')
+ self._add_curve('secp384r1', 'SECP384R1')
+ self._add_curve('secp521r1', 'SECP521R1')
+ self._add_curve('secp192r1', 'SECP192R1', deprecated=True)
+ self._add_curve('sect163k1', 'SECT163K1', deprecated=True)
+ self._add_curve('sect163r2', 'SECT163R2', deprecated=True)
+ self._add_curve('sect233k1', 'SECT233K1', deprecated=True)
+ self._add_curve('sect233r1', 'SECT233R1', deprecated=True)
+ self._add_curve('sect283k1', 'SECT283K1', deprecated=True)
+ self._add_curve('sect283r1', 'SECT283R1', deprecated=True)
+ self._add_curve('sect409k1', 'SECT409K1', deprecated=True)
+ self._add_curve('sect409r1', 'SECT409R1', deprecated=True)
+ self._add_curve('sect571k1', 'SECT571K1', deprecated=True)
+ self._add_curve('sect571r1', 'SECT571R1', deprecated=True)
+ self._add_curve('brainpoolP256r1', 'BrainpoolP256R1', deprecated=True)
+ self._add_curve('brainpoolP384r1', 'BrainpoolP384R1', deprecated=True)
+ self._add_curve('brainpoolP512r1', 'BrainpoolP512R1', deprecated=True)
+
+ self.cryptography_backend = cryptography.hazmat.backends.default_backend()
+
+ if not CRYPTOGRAPHY_HAS_X25519 and self.type == 'X25519':
+ self.module.fail_json(msg='Your cryptography version does not support X25519')
+ if not CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519':
+ self.module.fail_json(msg='Your cryptography version does not support X25519 serialization')
+ if not CRYPTOGRAPHY_HAS_X448 and self.type == 'X448':
+ self.module.fail_json(msg='Your cryptography version does not support X448')
+ if not CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519':
+ self.module.fail_json(msg='Your cryptography version does not support Ed25519')
+ if not CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448':
+ self.module.fail_json(msg='Your cryptography version does not support Ed448')
+
+ def _get_wanted_format(self):
+ if self.format not in ('auto', 'auto_ignore'):
+ return self.format
+ if self.type in ('X25519', 'X448', 'Ed25519', 'Ed448'):
+ return 'pkcs8'
+ else:
+ return 'pkcs1'
+
+ def generate_private_key(self):
+ """(Re-)Generate private key."""
+ try:
+ if self.type == 'RSA':
+ self.private_key = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key(
+ public_exponent=65537, # OpenSSL always uses this
+ key_size=self.size,
+ backend=self.cryptography_backend
+ )
+ if self.type == 'DSA':
+ self.private_key = cryptography.hazmat.primitives.asymmetric.dsa.generate_private_key(
+ key_size=self.size,
+ backend=self.cryptography_backend
+ )
+ if CRYPTOGRAPHY_HAS_X25519_FULL and self.type == 'X25519':
+ self.private_key = cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.generate()
+ if CRYPTOGRAPHY_HAS_X448 and self.type == 'X448':
+ self.private_key = cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.generate()
+ if CRYPTOGRAPHY_HAS_ED25519 and self.type == 'Ed25519':
+ self.private_key = cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.generate()
+ if CRYPTOGRAPHY_HAS_ED448 and self.type == 'Ed448':
+ self.private_key = cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.generate()
+ if self.type == 'ECC' and self.curve in self.curves:
+ if self.curves[self.curve]['deprecated']:
+ self.module.warn('Elliptic curves of type {0} should not be used for new keys!'.format(self.curve))
+ self.private_key = cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(
+ curve=self.curves[self.curve]['create'](self.size),
+ backend=self.cryptography_backend
+ )
+ except cryptography.exceptions.UnsupportedAlgorithm as dummy:
+ self.module.fail_json(msg='Cryptography backend does not support the algorithm required for {0}'.format(self.type))
+
+ def get_private_key_data(self):
+ """Return bytes for self.private_key"""
+ # Select export format and encoding
+ try:
+ export_format = self._get_wanted_format()
+ export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM
+ if export_format == 'pkcs1':
+ # "TraditionalOpenSSL" format is PKCS1
+ export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
+ elif export_format == 'pkcs8':
+ export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
+ elif export_format == 'raw':
+ export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.Raw
+ export_encoding = cryptography.hazmat.primitives.serialization.Encoding.Raw
+ except AttributeError:
+ self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format))
+
+ # Select key encryption
+ encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption()
+ if self.cipher and self.passphrase:
+ if self.cipher == 'auto':
+ encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.passphrase))
+ else:
+ self.module.fail_json(msg='Cryptography backend can only use "auto" for cipher option.')
+
+ # Serialize key
+ try:
+ return self.private_key.private_bytes(
+ encoding=export_encoding,
+ format=export_format,
+ encryption_algorithm=encryption_algorithm
+ )
+ except ValueError as dummy:
+ self.module.fail_json(
+ msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format)
+ )
+ except Exception as dummy:
+ self.module.fail_json(
+ msg='Error while serializing the private key in the required format "{0}"'.format(self.format),
+ exception=traceback.format_exc()
+ )
+
+ def _load_privatekey(self):
+ data = self.existing_private_key_bytes
+ try:
+ # Interpret bytes depending on format.
+ format = identify_private_key_format(data)
+ if format == 'raw':
+ if len(data) == 56 and CRYPTOGRAPHY_HAS_X448:
+ return cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data)
+ if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448:
+ return cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data)
+ if len(data) == 32:
+ if CRYPTOGRAPHY_HAS_X25519 and (self.type == 'X25519' or not CRYPTOGRAPHY_HAS_ED25519):
+ return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
+ if CRYPTOGRAPHY_HAS_ED25519 and (self.type == 'Ed25519' or not CRYPTOGRAPHY_HAS_X25519):
+ return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
+ if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519:
+ try:
+ return cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
+ except Exception:
+ return cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
+ raise PrivateKeyError('Cannot load raw key')
+ else:
+ return cryptography.hazmat.primitives.serialization.load_pem_private_key(
+ data,
+ None if self.passphrase is None else to_bytes(self.passphrase),
+ backend=self.cryptography_backend
+ )
+ except Exception as e:
+ raise PrivateKeyError(e)
+
+ def _ensure_existing_private_key_loaded(self):
+ if self.existing_private_key is None and self.has_existing():
+ self.existing_private_key = self._load_privatekey()
+
+ def _check_passphrase(self):
+ try:
+ format = identify_private_key_format(self.existing_private_key_bytes)
+ if format == 'raw':
+ # Raw keys cannot be encrypted. To avoid incompatibilities, we try to
+ # actually load the key (and return False when this fails).
+ self._load_privatekey()
+ # Loading the key succeeded. Only return True when no passphrase was
+ # provided.
+ return self.passphrase is None
+ else:
+ return cryptography.hazmat.primitives.serialization.load_pem_private_key(
+ self.existing_private_key_bytes,
+ None if self.passphrase is None else to_bytes(self.passphrase),
+ backend=self.cryptography_backend
+ )
+ except Exception as dummy:
+ return False
+
+ def _check_size_and_type(self):
+ if isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
+ return self.type == 'RSA' and self.size == self.existing_private_key.key_size
+ if isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
+ return self.type == 'DSA' and self.size == self.existing_private_key.key_size
+ if CRYPTOGRAPHY_HAS_X25519 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey):
+ return self.type == 'X25519'
+ if CRYPTOGRAPHY_HAS_X448 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey):
+ return self.type == 'X448'
+ if CRYPTOGRAPHY_HAS_ED25519 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
+ return self.type == 'Ed25519'
+ if CRYPTOGRAPHY_HAS_ED448 and isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
+ return self.type == 'Ed448'
+ if isinstance(self.existing_private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
+ if self.type != 'ECC':
+ return False
+ if self.curve not in self.curves:
+ return False
+ return self.curves[self.curve]['verify'](self.existing_private_key)
+
+ return False
+
+ def _check_format(self):
+ if self.format == 'auto_ignore':
+ return True
+ try:
+ format = identify_private_key_format(self.existing_private_key_bytes)
+ return format == self._get_wanted_format()
+ except Exception as dummy:
+ return False
+
+
+def select_backend(module, backend):
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
+
+ # Decision
+ if can_use_cryptography:
+ backend = 'cryptography'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect the required Python library "
+ "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
+ if backend == 'cryptography':
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+ return backend, PrivateKeyCryptographyBackend(module)
+ else:
+ raise Exception('Unsupported value for backend: {0}'.format(backend))
+
+
+def get_privatekey_argument_spec():
+ return ArgumentSpec(
+ argument_spec=dict(
+ size=dict(type='int', default=4096),
+ type=dict(type='str', default='RSA', choices=[
+ 'DSA', 'ECC', 'Ed25519', 'Ed448', 'RSA', 'X25519', 'X448'
+ ]),
+ curve=dict(type='str', choices=[
+ 'secp224r1', 'secp256k1', 'secp256r1', 'secp384r1', 'secp521r1',
+ 'secp192r1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1',
+ 'sect163k1', 'sect163r2', 'sect233k1', 'sect233r1', 'sect283k1',
+ 'sect283r1', 'sect409k1', 'sect409r1', 'sect571k1', 'sect571r1',
+ ]),
+ passphrase=dict(type='str', no_log=True),
+ cipher=dict(type='str'),
+ format=dict(type='str', default='auto_ignore', choices=['pkcs1', 'pkcs8', 'raw', 'auto', 'auto_ignore']),
+ format_mismatch=dict(type='str', default='regenerate', choices=['regenerate', 'convert']),
+ select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
+ regenerate=dict(
+ type='str',
+ default='full_idempotence',
+ choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
+ ),
+ ),
+ required_together=[
+ ['cipher', 'passphrase']
+ ],
+ required_if=[
+ ['type', 'ECC', ['curve']],
+ ],
+ )
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_convert.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_convert.py
new file mode 100644
index 00000000..905ca70f
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_convert.py
@@ -0,0 +1,236 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+import traceback
+
+from ansible.module_utils import six
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.common.text.converters import to_bytes
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.io import (
+ load_file,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ CRYPTOGRAPHY_HAS_X25519,
+ CRYPTOGRAPHY_HAS_X448,
+ CRYPTOGRAPHY_HAS_ED25519,
+ CRYPTOGRAPHY_HAS_ED448,
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
+ cryptography_compare_private_keys,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
+ identify_private_key_format,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.common import ArgumentSpec
+
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ import cryptography.exceptions
+ import cryptography.hazmat.backends
+ import cryptography.hazmat.primitives.serialization
+ import cryptography.hazmat.primitives.asymmetric.rsa
+ import cryptography.hazmat.primitives.asymmetric.dsa
+ import cryptography.hazmat.primitives.asymmetric.ec
+ import cryptography.hazmat.primitives.asymmetric.utils
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+
+class PrivateKeyError(OpenSSLObjectError):
+ pass
+
+
+# From the object called `module`, only the following properties are used:
+#
+# - module.params[]
+# - module.warn(msg: str)
+# - module.fail_json(msg: str, **kwargs)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class PrivateKeyConvertBackend:
+ def __init__(self, module, backend):
+ self.module = module
+ self.src_path = module.params['src_path']
+ self.src_content = module.params['src_content']
+ self.src_passphrase = module.params['src_passphrase']
+ self.format = module.params['format']
+ self.dest_passphrase = module.params['dest_passphrase']
+ self.backend = backend
+
+ self.src_private_key = None
+ if self.src_path is not None:
+ self.src_private_key_bytes = load_file(self.src_path, module)
+ else:
+ self.src_private_key_bytes = self.src_content.encode('utf-8')
+
+ self.dest_private_key = None
+ self.dest_private_key_bytes = None
+
+ @abc.abstractmethod
+ def get_private_key_data(self):
+ """Return bytes for self.src_private_key in output format."""
+ pass
+
+ def set_existing_destination(self, privatekey_bytes):
+ """Set existing private key bytes. None indicates that the key does not exist."""
+ self.dest_private_key_bytes = privatekey_bytes
+
+ def has_existing_destination(self):
+ """Query whether an existing private key is/has been there."""
+ return self.dest_private_key_bytes is not None
+
+ @abc.abstractmethod
+ def _load_private_key(self, data, passphrase, current_hint=None):
+ """Check whether data cna be loaded as a private key with the provided passphrase. Return tuple (type, private_key)."""
+ pass
+
+ def needs_conversion(self):
+ """Check whether a conversion is necessary. Must only be called if needs_regeneration() returned False."""
+ dummy, self.src_private_key = self._load_private_key(self.src_private_key_bytes, self.src_passphrase)
+
+ if not self.has_existing_destination():
+ return True
+
+ try:
+ format, self.dest_private_key = self._load_private_key(self.dest_private_key_bytes, self.dest_passphrase, current_hint=self.src_private_key)
+ except Exception:
+ return True
+
+ return format != self.format or not cryptography_compare_private_keys(self.dest_private_key, self.src_private_key)
+
+ def dump(self):
+ """Serialize the object into a dictionary."""
+ return {}
+
+
+# Implementation with using cryptography
+class PrivateKeyConvertCryptographyBackend(PrivateKeyConvertBackend):
+ def __init__(self, module):
+ super(PrivateKeyConvertCryptographyBackend, self).__init__(module=module, backend='cryptography')
+
+ self.cryptography_backend = cryptography.hazmat.backends.default_backend()
+
+ def get_private_key_data(self):
+ """Return bytes for self.src_private_key in output format"""
+ # Select export format and encoding
+ try:
+ export_encoding = cryptography.hazmat.primitives.serialization.Encoding.PEM
+ if self.format == 'pkcs1':
+ # "TraditionalOpenSSL" format is PKCS1
+ export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.TraditionalOpenSSL
+ elif self.format == 'pkcs8':
+ export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.PKCS8
+ elif self.format == 'raw':
+ export_format = cryptography.hazmat.primitives.serialization.PrivateFormat.Raw
+ export_encoding = cryptography.hazmat.primitives.serialization.Encoding.Raw
+ except AttributeError:
+ self.module.fail_json(msg='Cryptography backend does not support the selected output format "{0}"'.format(self.format))
+
+ # Select key encryption
+ encryption_algorithm = cryptography.hazmat.primitives.serialization.NoEncryption()
+ if self.dest_passphrase:
+ encryption_algorithm = cryptography.hazmat.primitives.serialization.BestAvailableEncryption(to_bytes(self.dest_passphrase))
+
+ # Serialize key
+ try:
+ return self.src_private_key.private_bytes(
+ encoding=export_encoding,
+ format=export_format,
+ encryption_algorithm=encryption_algorithm
+ )
+ except ValueError as dummy:
+ self.module.fail_json(
+ msg='Cryptography backend cannot serialize the private key in the required format "{0}"'.format(self.format)
+ )
+ except Exception as dummy:
+ self.module.fail_json(
+ msg='Error while serializing the private key in the required format "{0}"'.format(self.format),
+ exception=traceback.format_exc()
+ )
+
+ def _load_private_key(self, data, passphrase, current_hint=None):
+ try:
+ # Interpret bytes depending on format.
+ format = identify_private_key_format(data)
+ if format == 'raw':
+ if passphrase is not None:
+ raise PrivateKeyError('Cannot load raw key with passphrase')
+ if len(data) == 56 and CRYPTOGRAPHY_HAS_X448:
+ return format, cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey.from_private_bytes(data)
+ if len(data) == 57 and CRYPTOGRAPHY_HAS_ED448:
+ return format, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey.from_private_bytes(data)
+ if len(data) == 32:
+ if CRYPTOGRAPHY_HAS_X25519 and not CRYPTOGRAPHY_HAS_ED25519:
+ return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
+ if CRYPTOGRAPHY_HAS_ED25519 and not CRYPTOGRAPHY_HAS_X25519:
+ return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
+ if CRYPTOGRAPHY_HAS_X25519 and CRYPTOGRAPHY_HAS_ED25519:
+ if isinstance(current_hint, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey):
+ try:
+ return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
+ except Exception:
+ return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
+ else:
+ try:
+ return format, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey.from_private_bytes(data)
+ except Exception:
+ return format, cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey.from_private_bytes(data)
+ raise PrivateKeyError('Cannot load raw key')
+ else:
+ return format, cryptography.hazmat.primitives.serialization.load_pem_private_key(
+ data,
+ None if passphrase is None else to_bytes(passphrase),
+ backend=self.cryptography_backend
+ )
+ except Exception as e:
+ raise PrivateKeyError(e)
+
+
+def select_backend(module):
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+ return PrivateKeyConvertCryptographyBackend(module)
+
+
+def get_privatekey_argument_spec():
+ return ArgumentSpec(
+ argument_spec=dict(
+ src_path=dict(type='path'),
+ src_content=dict(type='str'),
+ src_passphrase=dict(type='str', no_log=True),
+ dest_passphrase=dict(type='str', no_log=True),
+ format=dict(type='str', required=True, choices=['pkcs1', 'pkcs8', 'raw']),
+ ),
+ mutually_exclusive=[
+ ['src_path', 'src_content'],
+ ],
+ required_one_of=[
+ ['src_path', 'src_content'],
+ ],
+ )
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_info.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_info.py
new file mode 100644
index 00000000..d87b9c2b
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/privatekey_info.py
@@ -0,0 +1,287 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+import traceback
+
+from ansible.module_utils import six
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.common.text.converters import to_native, to_bytes
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ CRYPTOGRAPHY_HAS_ED25519,
+ CRYPTOGRAPHY_HAS_ED448,
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ load_privatekey,
+ get_fingerprint_of_bytes,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
+ binary_exp_mod,
+ quick_is_not_prime,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
+ _get_cryptography_public_key_info,
+)
+
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ from cryptography.hazmat.primitives import serialization
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+SIGNATURE_TEST_DATA = b'1234'
+
+
+def _get_cryptography_private_key_info(key, need_private_key_data=False):
+ key_type, key_public_data = _get_cryptography_public_key_info(key.public_key())
+ key_private_data = dict()
+ if need_private_key_data:
+ if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
+ private_numbers = key.private_numbers()
+ key_private_data['p'] = private_numbers.p
+ key_private_data['q'] = private_numbers.q
+ key_private_data['exponent'] = private_numbers.d
+ elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
+ private_numbers = key.private_numbers()
+ key_private_data['x'] = private_numbers.x
+ elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
+ private_numbers = key.private_numbers()
+ key_private_data['multiplier'] = private_numbers.private_value
+ return key_type, key_public_data, key_private_data
+
+
+def _check_dsa_consistency(key_public_data, key_private_data):
+ # Get parameters
+ p = key_public_data.get('p')
+ q = key_public_data.get('q')
+ g = key_public_data.get('g')
+ y = key_public_data.get('y')
+ x = key_private_data.get('x')
+ for v in (p, q, g, y, x):
+ if v is None:
+ return None
+ # Make sure that g is not 0, 1 or -1 in Z/pZ
+ if g < 2 or g >= p - 1:
+ return False
+ # Make sure that x is in range
+ if x < 1 or x >= q:
+ return False
+ # Check whether q divides p-1
+ if (p - 1) % q != 0:
+ return False
+ # Check that g**q mod p == 1
+ if binary_exp_mod(g, q, p) != 1:
+ return False
+ # Check whether g**x mod p == y
+ if binary_exp_mod(g, x, p) != y:
+ return False
+ # Check (quickly) whether p or q are not primes
+ if quick_is_not_prime(q) or quick_is_not_prime(p):
+ return False
+ return True
+
+
+def _is_cryptography_key_consistent(key, key_public_data, key_private_data):
+ if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
+ return bool(key._backend._lib.RSA_check_key(key._rsa_cdata))
+ if isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
+ result = _check_dsa_consistency(key_public_data, key_private_data)
+ if result is not None:
+ return result
+ try:
+ signature = key.sign(SIGNATURE_TEST_DATA, cryptography.hazmat.primitives.hashes.SHA256())
+ except AttributeError:
+ # sign() was added in cryptography 1.5, but we support older versions
+ return None
+ try:
+ key.public_key().verify(
+ signature,
+ SIGNATURE_TEST_DATA,
+ cryptography.hazmat.primitives.hashes.SHA256()
+ )
+ return True
+ except cryptography.exceptions.InvalidSignature:
+ return False
+ if isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
+ try:
+ signature = key.sign(
+ SIGNATURE_TEST_DATA,
+ cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
+ )
+ except AttributeError:
+ # sign() was added in cryptography 1.5, but we support older versions
+ return None
+ try:
+ key.public_key().verify(
+ signature,
+ SIGNATURE_TEST_DATA,
+ cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cryptography.hazmat.primitives.hashes.SHA256())
+ )
+ return True
+ except cryptography.exceptions.InvalidSignature:
+ return False
+ has_simple_sign_function = False
+ if CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
+ has_simple_sign_function = True
+ if CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
+ has_simple_sign_function = True
+ if has_simple_sign_function:
+ signature = key.sign(SIGNATURE_TEST_DATA)
+ try:
+ key.public_key().verify(signature, SIGNATURE_TEST_DATA)
+ return True
+ except cryptography.exceptions.InvalidSignature:
+ return False
+ # For X25519 and X448, there's no test yet.
+ return None
+
+
+class PrivateKeyConsistencyError(OpenSSLObjectError):
+ def __init__(self, msg, result):
+ super(PrivateKeyConsistencyError, self).__init__(msg)
+ self.error_message = msg
+ self.result = result
+
+
+class PrivateKeyParseError(OpenSSLObjectError):
+ def __init__(self, msg, result):
+ super(PrivateKeyParseError, self).__init__(msg)
+ self.error_message = msg
+ self.result = result
+
+
+@six.add_metaclass(abc.ABCMeta)
+class PrivateKeyInfoRetrieval(object):
+ def __init__(self, module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False):
+ # content must be a bytes string
+ self.module = module
+ self.backend = backend
+ self.content = content
+ self.passphrase = passphrase
+ self.return_private_key_data = return_private_key_data
+ self.check_consistency = check_consistency
+
+ @abc.abstractmethod
+ def _get_public_key(self, binary):
+ pass
+
+ @abc.abstractmethod
+ def _get_key_info(self, need_private_key_data=False):
+ pass
+
+ @abc.abstractmethod
+ def _is_key_consistent(self, key_public_data, key_private_data):
+ pass
+
+ def get_info(self, prefer_one_fingerprint=False):
+ result = dict(
+ can_parse_key=False,
+ key_is_consistent=None,
+ )
+ priv_key_detail = self.content
+ try:
+ self.key = load_privatekey(
+ path=None,
+ content=priv_key_detail,
+ passphrase=to_bytes(self.passphrase) if self.passphrase is not None else self.passphrase,
+ backend=self.backend
+ )
+ result['can_parse_key'] = True
+ except OpenSSLObjectError as exc:
+ raise PrivateKeyParseError(to_native(exc), result)
+
+ result['public_key'] = to_native(self._get_public_key(binary=False))
+ pk = self._get_public_key(binary=True)
+ result['public_key_fingerprints'] = get_fingerprint_of_bytes(
+ pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict()
+
+ key_type, key_public_data, key_private_data = self._get_key_info(
+ need_private_key_data=self.return_private_key_data or self.check_consistency)
+ result['type'] = key_type
+ result['public_data'] = key_public_data
+ if self.return_private_key_data:
+ result['private_data'] = key_private_data
+
+ if self.check_consistency:
+ result['key_is_consistent'] = self._is_key_consistent(key_public_data, key_private_data)
+ if result['key_is_consistent'] is False:
+ # Only fail when it is False, to avoid to fail on None (which means "we do not know")
+ msg = (
+ "Private key is not consistent! (See "
+ "https://blog.hboeck.de/archives/888-How-I-tricked-Symantec-with-a-Fake-Private-Key.html)"
+ )
+ raise PrivateKeyConsistencyError(msg, result)
+ return result
+
+
+class PrivateKeyInfoRetrievalCryptography(PrivateKeyInfoRetrieval):
+ """Validate the supplied private key, using the cryptography backend"""
+ def __init__(self, module, content, **kwargs):
+ super(PrivateKeyInfoRetrievalCryptography, self).__init__(module, 'cryptography', content, **kwargs)
+
+ def _get_public_key(self, binary):
+ return self.key.public_key().public_bytes(
+ serialization.Encoding.DER if binary else serialization.Encoding.PEM,
+ serialization.PublicFormat.SubjectPublicKeyInfo
+ )
+
+ def _get_key_info(self, need_private_key_data=False):
+ return _get_cryptography_private_key_info(self.key, need_private_key_data=need_private_key_data)
+
+ def _is_key_consistent(self, key_public_data, key_private_data):
+ return _is_cryptography_key_consistent(self.key, key_public_data, key_private_data)
+
+
+def get_privatekey_info(module, backend, content, passphrase=None, return_private_key_data=False, prefer_one_fingerprint=False):
+ if backend == 'cryptography':
+ info = PrivateKeyInfoRetrievalCryptography(
+ module, content, passphrase=passphrase, return_private_key_data=return_private_key_data)
+ return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
+
+
+def select_backend(module, backend, content, passphrase=None, return_private_key_data=False, check_consistency=False):
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
+
+ # Try cryptography
+ if can_use_cryptography:
+ backend = 'cryptography'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect the required Python library "
+ "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
+
+ if backend == 'cryptography':
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+ return backend, PrivateKeyInfoRetrievalCryptography(
+ module, content, passphrase=passphrase, return_private_key_data=return_private_key_data, check_consistency=check_consistency)
+ else:
+ raise ValueError('Unsupported value for backend: {0}'.format(backend))
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/publickey_info.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/publickey_info.py
new file mode 100644
index 00000000..d381d206
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/module_backends/publickey_info.py
@@ -0,0 +1,168 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020-2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+import traceback
+
+from ansible.module_utils import six
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ CRYPTOGRAPHY_HAS_X25519,
+ CRYPTOGRAPHY_HAS_X448,
+ CRYPTOGRAPHY_HAS_ED25519,
+ CRYPTOGRAPHY_HAS_ED448,
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ get_fingerprint_of_bytes,
+ load_publickey,
+)
+
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ from cryptography.hazmat.primitives import serialization
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+
+def _get_cryptography_public_key_info(key):
+ key_public_data = dict()
+ if isinstance(key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
+ key_type = 'RSA'
+ public_numbers = key.public_numbers()
+ key_public_data['size'] = key.key_size
+ key_public_data['modulus'] = public_numbers.n
+ key_public_data['exponent'] = public_numbers.e
+ elif isinstance(key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey):
+ key_type = 'DSA'
+ parameter_numbers = key.parameters().parameter_numbers()
+ public_numbers = key.public_numbers()
+ key_public_data['size'] = key.key_size
+ key_public_data['p'] = parameter_numbers.p
+ key_public_data['q'] = parameter_numbers.q
+ key_public_data['g'] = parameter_numbers.g
+ key_public_data['y'] = public_numbers.y
+ elif CRYPTOGRAPHY_HAS_X25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey):
+ key_type = 'X25519'
+ elif CRYPTOGRAPHY_HAS_X448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey):
+ key_type = 'X448'
+ elif CRYPTOGRAPHY_HAS_ED25519 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
+ key_type = 'Ed25519'
+ elif CRYPTOGRAPHY_HAS_ED448 and isinstance(key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
+ key_type = 'Ed448'
+ elif isinstance(key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
+ key_type = 'ECC'
+ public_numbers = key.public_numbers()
+ key_public_data['curve'] = key.curve.name
+ key_public_data['x'] = public_numbers.x
+ key_public_data['y'] = public_numbers.y
+ key_public_data['exponent_size'] = key.curve.key_size
+ else:
+ key_type = 'unknown ({0})'.format(type(key))
+ return key_type, key_public_data
+
+
+class PublicKeyParseError(OpenSSLObjectError):
+ def __init__(self, msg, result):
+ super(PublicKeyParseError, self).__init__(msg)
+ self.error_message = msg
+ self.result = result
+
+
+@six.add_metaclass(abc.ABCMeta)
+class PublicKeyInfoRetrieval(object):
+ def __init__(self, module, backend, content=None, key=None):
+ # content must be a bytes string
+ self.module = module
+ self.backend = backend
+ self.content = content
+ self.key = key
+
+ @abc.abstractmethod
+ def _get_public_key(self, binary):
+ pass
+
+ @abc.abstractmethod
+ def _get_key_info(self):
+ pass
+
+ def get_info(self, prefer_one_fingerprint=False):
+ result = dict()
+ if self.key is None:
+ try:
+ self.key = load_publickey(content=self.content, backend=self.backend)
+ except OpenSSLObjectError as e:
+ raise PublicKeyParseError(to_native(e), {})
+
+ pk = self._get_public_key(binary=True)
+ result['fingerprints'] = get_fingerprint_of_bytes(
+ pk, prefer_one=prefer_one_fingerprint) if pk is not None else dict()
+
+ key_type, key_public_data = self._get_key_info()
+ result['type'] = key_type
+ result['public_data'] = key_public_data
+ return result
+
+
+class PublicKeyInfoRetrievalCryptography(PublicKeyInfoRetrieval):
+ """Validate the supplied public key, using the cryptography backend"""
+ def __init__(self, module, content=None, key=None):
+ super(PublicKeyInfoRetrievalCryptography, self).__init__(module, 'cryptography', content=content, key=key)
+
+ def _get_public_key(self, binary):
+ return self.key.public_bytes(
+ serialization.Encoding.DER if binary else serialization.Encoding.PEM,
+ serialization.PublicFormat.SubjectPublicKeyInfo
+ )
+
+ def _get_key_info(self):
+ return _get_cryptography_public_key_info(self.key)
+
+
+def get_publickey_info(module, backend, content=None, key=None, prefer_one_fingerprint=False):
+ if backend == 'cryptography':
+ info = PublicKeyInfoRetrievalCryptography(module, content=content, key=key)
+ return info.get_info(prefer_one_fingerprint=prefer_one_fingerprint)
+
+
+def select_backend(module, backend, content=None, key=None):
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
+
+ # Try cryptography
+ if can_use_cryptography:
+ backend = 'cryptography'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect any of the required Python libraries "
+ "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
+
+ if backend == 'cryptography':
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+ return backend, PublicKeyInfoRetrievalCryptography(module, content=content, key=key)
+ else:
+ raise ValueError('Unsupported value for backend: {0}'.format(backend))
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/openssh.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/openssh.py
new file mode 100644
index 00000000..98247538
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/openssh.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020, Doug Stanley <doug+ansible@technologixllc.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+# This import is only to maintain backwards compatibility
+from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import ( # noqa: F401, pylint: disable=unused-import
+ parse_openssh_version
+)
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/pem.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/pem.py
new file mode 100644
index 00000000..4dc9745f
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/pem.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+PEM_START = '-----BEGIN '
+PEM_END = '-----'
+PKCS8_PRIVATEKEY_NAMES = ('PRIVATE KEY', 'ENCRYPTED PRIVATE KEY')
+PKCS1_PRIVATEKEY_SUFFIX = ' PRIVATE KEY'
+
+
+def identify_pem_format(content):
+ '''Given the contents of a binary file, tests whether this could be a PEM file.'''
+ try:
+ lines = content.decode('utf-8').splitlines(False)
+ if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END):
+ return True
+ except UnicodeDecodeError:
+ pass
+ return False
+
+
+def identify_private_key_format(content):
+ '''Given the contents of a private key file, identifies its format.'''
+ # See https://github.com/openssl/openssl/blob/master/crypto/pem/pem_pkey.c#L40-L85
+ # (PEM_read_bio_PrivateKey)
+ # and https://github.com/openssl/openssl/blob/master/include/openssl/pem.h#L46-L47
+ # (PEM_STRING_PKCS8, PEM_STRING_PKCS8INF)
+ try:
+ lines = content.decode('utf-8').splitlines(False)
+ if lines[0].startswith(PEM_START) and lines[0].endswith(PEM_END) and len(lines[0]) > len(PEM_START) + len(PEM_END):
+ name = lines[0][len(PEM_START):-len(PEM_END)]
+ if name in PKCS8_PRIVATEKEY_NAMES:
+ return 'pkcs8'
+ if len(name) > len(PKCS1_PRIVATEKEY_SUFFIX) and name.endswith(PKCS1_PRIVATEKEY_SUFFIX):
+ return 'pkcs1'
+ return 'unknown-pem'
+ except UnicodeDecodeError:
+ pass
+ return 'raw'
+
+
+def split_pem_list(text, keep_inbetween=False):
+ '''
+ Split concatenated PEM objects into a list of strings, where each is one PEM object.
+ '''
+ result = []
+ current = [] if keep_inbetween else None
+ for line in text.splitlines(True):
+ if line.strip():
+ if not keep_inbetween and line.startswith('-----BEGIN '):
+ current = []
+ if current is not None:
+ current.append(line)
+ if line.startswith('-----END '):
+ result.append(''.join(current))
+ current = [] if keep_inbetween else None
+ return result
+
+
+def extract_first_pem(text):
+ '''
+ Given one PEM or multiple concatenated PEM objects, return only the first one, or None if there is none.
+ '''
+ all_pems = split_pem_list(text)
+ if not all_pems:
+ return None
+ return all_pems[0]
diff --git a/ansible_collections/community/crypto/plugins/module_utils/crypto/support.py b/ansible_collections/community/crypto/plugins/module_utils/crypto/support.py
new file mode 100644
index 00000000..ad8f1610
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/crypto/support.py
@@ -0,0 +1,401 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+import datetime
+import errno
+import hashlib
+import os
+import re
+
+from ansible.module_utils import six
+from ansible.module_utils.common.text.converters import to_native, to_bytes
+
+try:
+ from OpenSSL import crypto
+ HAS_PYOPENSSL = True
+except (ImportError, AttributeError):
+ # Error handled in the calling module.
+ HAS_PYOPENSSL = False
+
+try:
+ from cryptography import x509
+ from cryptography.hazmat.backends import default_backend as cryptography_backend
+ from cryptography.hazmat.primitives.serialization import load_pem_private_key
+ from cryptography.hazmat.primitives import hashes
+ from cryptography.hazmat.primitives import serialization
+except ImportError:
+ # Error handled in the calling module.
+ pass
+
+from .basic import (
+ OpenSSLObjectError,
+ OpenSSLBadPassphraseError,
+)
+
+
+# This list of preferred fingerprints is used when prefer_one=True is supplied to the
+# fingerprinting methods.
+PREFERRED_FINGERPRINTS = (
+ 'sha256', 'sha3_256', 'sha512', 'sha3_512', 'sha384', 'sha3_384', 'sha1', 'md5'
+)
+
+
+def get_fingerprint_of_bytes(source, prefer_one=False):
+ """Generate the fingerprint of the given bytes."""
+
+ fingerprint = {}
+
+ try:
+ algorithms = hashlib.algorithms
+ except AttributeError:
+ try:
+ algorithms = hashlib.algorithms_guaranteed
+ except AttributeError:
+ return None
+
+ if prefer_one:
+ # Sort algorithms to have the ones in PREFERRED_FINGERPRINTS at the beginning
+ prefered_algorithms = [algorithm for algorithm in PREFERRED_FINGERPRINTS if algorithm in algorithms]
+ prefered_algorithms += sorted([algorithm for algorithm in algorithms if algorithm not in PREFERRED_FINGERPRINTS])
+ algorithms = prefered_algorithms
+
+ for algo in algorithms:
+ f = getattr(hashlib, algo)
+ try:
+ h = f(source)
+ except ValueError:
+ # This can happen for hash algorithms not supported in FIPS mode
+ # (https://github.com/ansible/ansible/issues/67213)
+ continue
+ try:
+ # Certain hash functions have a hexdigest() which expects a length parameter
+ pubkey_digest = h.hexdigest()
+ except TypeError:
+ pubkey_digest = h.hexdigest(32)
+ fingerprint[algo] = ':'.join(pubkey_digest[i:i + 2] for i in range(0, len(pubkey_digest), 2))
+ if prefer_one:
+ break
+
+ return fingerprint
+
+
+def get_fingerprint_of_privatekey(privatekey, backend='cryptography', prefer_one=False):
+ """Generate the fingerprint of the public key. """
+
+ if backend == 'cryptography':
+ publickey = privatekey.public_key().public_bytes(
+ serialization.Encoding.DER,
+ serialization.PublicFormat.SubjectPublicKeyInfo
+ )
+
+ return get_fingerprint_of_bytes(publickey, prefer_one=prefer_one)
+
+
+def get_fingerprint(path, passphrase=None, content=None, backend='cryptography', prefer_one=False):
+ """Generate the fingerprint of the public key. """
+
+ privatekey = load_privatekey(path, passphrase=passphrase, content=content, check_passphrase=False, backend=backend)
+
+ return get_fingerprint_of_privatekey(privatekey, backend=backend, prefer_one=prefer_one)
+
+
+def load_privatekey(path, passphrase=None, check_passphrase=True, content=None, backend='cryptography'):
+ """Load the specified OpenSSL private key.
+
+ The content can also be specified via content; in that case,
+ this function will not load the key from disk.
+ """
+
+ try:
+ if content is None:
+ with open(path, 'rb') as b_priv_key_fh:
+ priv_key_detail = b_priv_key_fh.read()
+ else:
+ priv_key_detail = content
+ except (IOError, OSError) as exc:
+ raise OpenSSLObjectError(exc)
+
+ if backend == 'pyopenssl':
+
+ # First try: try to load with real passphrase (resp. empty string)
+ # Will work if this is the correct passphrase, or the key is not
+ # password-protected.
+ try:
+ result = crypto.load_privatekey(crypto.FILETYPE_PEM,
+ priv_key_detail,
+ to_bytes(passphrase or ''))
+ except crypto.Error as e:
+ if len(e.args) > 0 and len(e.args[0]) > 0:
+ if e.args[0][0][2] in ('bad decrypt', 'bad password read'):
+ # This happens in case we have the wrong passphrase.
+ if passphrase is not None:
+ raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key!')
+ else:
+ raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!')
+ raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
+ if check_passphrase:
+ # Next we want to make sure that the key is actually protected by
+ # a passphrase (in case we did try the empty string before, make
+ # sure that the key is not protected by the empty string)
+ try:
+ crypto.load_privatekey(crypto.FILETYPE_PEM,
+ priv_key_detail,
+ to_bytes('y' if passphrase == 'x' else 'x'))
+ if passphrase is not None:
+ # Since we can load the key without an exception, the
+ # key is not password-protected
+ raise OpenSSLBadPassphraseError('Passphrase provided, but private key is not password-protected!')
+ except crypto.Error as e:
+ if passphrase is None and len(e.args) > 0 and len(e.args[0]) > 0:
+ if e.args[0][0][2] in ('bad decrypt', 'bad password read'):
+ # The key is obviously protected by the empty string.
+ # Do not do this at home (if it's possible at all)...
+ raise OpenSSLBadPassphraseError('No passphrase provided, but private key is password-protected!')
+ elif backend == 'cryptography':
+ try:
+ result = load_pem_private_key(priv_key_detail,
+ None if passphrase is None else to_bytes(passphrase),
+ cryptography_backend())
+ except TypeError:
+ raise OpenSSLBadPassphraseError('Wrong or empty passphrase provided for private key')
+ except ValueError:
+ raise OpenSSLBadPassphraseError('Wrong passphrase provided for private key')
+
+ return result
+
+
+def load_publickey(path=None, content=None, backend=None):
+ if content is None:
+ if path is None:
+ raise OpenSSLObjectError('Must provide either path or content')
+ try:
+ with open(path, 'rb') as b_priv_key_fh:
+ content = b_priv_key_fh.read()
+ except (IOError, OSError) as exc:
+ raise OpenSSLObjectError(exc)
+
+ if backend == 'cryptography':
+ try:
+ return serialization.load_pem_public_key(content, backend=cryptography_backend())
+ except Exception as e:
+ raise OpenSSLObjectError('Error while deserializing key: {0}'.format(e))
+
+
+def load_certificate(path, content=None, backend='cryptography'):
+ """Load the specified certificate."""
+
+ try:
+ if content is None:
+ with open(path, 'rb') as cert_fh:
+ cert_content = cert_fh.read()
+ else:
+ cert_content = content
+ except (IOError, OSError) as exc:
+ raise OpenSSLObjectError(exc)
+ if backend == 'pyopenssl':
+ return crypto.load_certificate(crypto.FILETYPE_PEM, cert_content)
+ elif backend == 'cryptography':
+ try:
+ return x509.load_pem_x509_certificate(cert_content, cryptography_backend())
+ except ValueError as exc:
+ raise OpenSSLObjectError(exc)
+
+
+def load_certificate_request(path, content=None, backend='cryptography'):
+ """Load the specified certificate signing request."""
+ try:
+ if content is None:
+ with open(path, 'rb') as csr_fh:
+ csr_content = csr_fh.read()
+ else:
+ csr_content = content
+ except (IOError, OSError) as exc:
+ raise OpenSSLObjectError(exc)
+ if backend == 'cryptography':
+ try:
+ return x509.load_pem_x509_csr(csr_content, cryptography_backend())
+ except ValueError as exc:
+ raise OpenSSLObjectError(exc)
+
+
+def parse_name_field(input_dict, name_field_name=None):
+ """Take a dict with key: value or key: list_of_values mappings and return a list of tuples"""
+ error_str = '{key}' if name_field_name is None else '{key} in {name}'
+
+ result = []
+ for key, value in input_dict.items():
+ if isinstance(value, list):
+ for entry in value:
+ if not isinstance(entry, six.string_types):
+ raise TypeError(('Values %s must be strings' % error_str).format(key=key, name=name_field_name))
+ if not entry:
+ raise ValueError(('Values for %s must not be empty strings' % error_str).format(key=key))
+ result.append((key, entry))
+ elif isinstance(value, six.string_types):
+ if not value:
+ raise ValueError(('Value for %s must not be an empty string' % error_str).format(key=key))
+ result.append((key, value))
+ else:
+ raise TypeError(('Value for %s must be either a string or a list of strings' % error_str).format(key=key))
+ return result
+
+
+def parse_ordered_name_field(input_list, name_field_name):
+ """Take a dict with key: value or key: list_of_values mappings and return a list of tuples"""
+
+ result = []
+ for index, entry in enumerate(input_list):
+ if len(entry) != 1:
+ raise ValueError(
+ 'Entry #{index} in {name} must be a dictionary with exactly one key-value pair'.format(
+ name=name_field_name, index=index + 1))
+ try:
+ result.extend(parse_name_field(entry, name_field_name=name_field_name))
+ except (TypeError, ValueError) as exc:
+ raise ValueError(
+ 'Error while processing entry #{index} in {name}: {error}'.format(
+ name=name_field_name, index=index + 1, error=exc))
+ return result
+
+
+def convert_relative_to_datetime(relative_time_string):
+ """Get a datetime.datetime or None from a string in the time format described in sshd_config(5)"""
+
+ parsed_result = re.match(
+ r"^(?P<prefix>[+-])((?P<weeks>\d+)[wW])?((?P<days>\d+)[dD])?((?P<hours>\d+)[hH])?((?P<minutes>\d+)[mM])?((?P<seconds>\d+)[sS]?)?$",
+ relative_time_string)
+
+ if parsed_result is None or len(relative_time_string) == 1:
+ # not matched or only a single "+" or "-"
+ return None
+
+ offset = datetime.timedelta(0)
+ if parsed_result.group("weeks") is not None:
+ offset += datetime.timedelta(weeks=int(parsed_result.group("weeks")))
+ if parsed_result.group("days") is not None:
+ offset += datetime.timedelta(days=int(parsed_result.group("days")))
+ if parsed_result.group("hours") is not None:
+ offset += datetime.timedelta(hours=int(parsed_result.group("hours")))
+ if parsed_result.group("minutes") is not None:
+ offset += datetime.timedelta(
+ minutes=int(parsed_result.group("minutes")))
+ if parsed_result.group("seconds") is not None:
+ offset += datetime.timedelta(
+ seconds=int(parsed_result.group("seconds")))
+
+ if parsed_result.group("prefix") == "+":
+ return datetime.datetime.utcnow() + offset
+ else:
+ return datetime.datetime.utcnow() - offset
+
+
+def get_relative_time_option(input_string, input_name, backend='cryptography'):
+ """Return an absolute timespec if a relative timespec or an ASN1 formatted
+ string is provided.
+
+ The return value will be a datetime object for the cryptography backend,
+ and a ASN1 formatted string for the pyopenssl backend."""
+ result = to_native(input_string)
+ if result is None:
+ raise OpenSSLObjectError(
+ 'The timespec "%s" for %s is not valid' %
+ input_string, input_name)
+ # Relative time
+ if result.startswith("+") or result.startswith("-"):
+ result_datetime = convert_relative_to_datetime(result)
+ if backend == 'pyopenssl':
+ return result_datetime.strftime("%Y%m%d%H%M%SZ")
+ elif backend == 'cryptography':
+ return result_datetime
+ # Absolute time
+ if backend == 'cryptography':
+ for date_fmt in ['%Y%m%d%H%M%SZ', '%Y%m%d%H%MZ', '%Y%m%d%H%M%S%z', '%Y%m%d%H%M%z']:
+ try:
+ return datetime.datetime.strptime(result, date_fmt)
+ except ValueError:
+ pass
+
+ raise OpenSSLObjectError(
+ 'The time spec "%s" for %s is invalid' %
+ (input_string, input_name)
+ )
+
+
+def select_message_digest(digest_string):
+ digest = None
+ if digest_string == 'sha256':
+ digest = hashes.SHA256()
+ elif digest_string == 'sha384':
+ digest = hashes.SHA384()
+ elif digest_string == 'sha512':
+ digest = hashes.SHA512()
+ elif digest_string == 'sha1':
+ digest = hashes.SHA1()
+ elif digest_string == 'md5':
+ digest = hashes.MD5()
+ return digest
+
+
+@six.add_metaclass(abc.ABCMeta)
+class OpenSSLObject(object):
+
+ def __init__(self, path, state, force, check_mode):
+ self.path = path
+ self.state = state
+ self.force = force
+ self.name = os.path.basename(path)
+ self.changed = False
+ self.check_mode = check_mode
+
+ def check(self, module, perms_required=True):
+ """Ensure the resource is in its desired state."""
+
+ def _check_state():
+ return os.path.exists(self.path)
+
+ def _check_perms(module):
+ file_args = module.load_file_common_arguments(module.params)
+ if module.check_file_absent_if_check_mode(file_args['path']):
+ return False
+ return not module.set_fs_attributes_if_different(file_args, False)
+
+ if not perms_required:
+ return _check_state()
+
+ return _check_state() and _check_perms(module)
+
+ @abc.abstractmethod
+ def dump(self):
+ """Serialize the object into a dictionary."""
+
+ pass
+
+ @abc.abstractmethod
+ def generate(self):
+ """Generate the resource."""
+
+ pass
+
+ def remove(self, module):
+ """Remove the resource from the filesystem."""
+ if self.check_mode:
+ if os.path.exists(self.path):
+ self.changed = True
+ return
+
+ try:
+ os.remove(self.path)
+ self.changed = True
+ except OSError as exc:
+ if exc.errno != errno.ENOENT:
+ raise OpenSSLObjectError(exc)
+ else:
+ pass
diff --git a/ansible_collections/community/crypto/plugins/module_utils/ecs/api.py b/ansible_collections/community/crypto/plugins/module_utils/ecs/api.py
new file mode 100644
index 00000000..bf8be58f
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/ecs/api.py
@@ -0,0 +1,346 @@
+# -*- coding: utf-8 -*-
+
+# This code is part of Ansible, but is an independent component.
+# This particular file snippet, and this file snippet only, is licensed under the
+# Modified BSD License. Modules you write using this snippet, which is embedded
+# dynamically by Ansible, still belong to the author of the module, and may assign
+# their own license to the complete work.
+#
+# Copyright (c), Entrust Datacard Corporation, 2019
+# Simplified BSD License (see LICENSES/BSD-2-Clause.txt or https://opensource.org/licenses/BSD-2-Clause)
+# SPDX-License-Identifier: BSD-2-Clause
+
+from __future__ import absolute_import, division, print_function
+
+__metaclass__ = type
+
+import json
+import os
+import re
+import traceback
+
+from ansible.module_utils.common.text.converters import to_text, to_native
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.six.moves.urllib.parse import urlencode
+from ansible.module_utils.six.moves.urllib.error import HTTPError
+from ansible.module_utils.urls import Request
+
+YAML_IMP_ERR = None
+try:
+ import yaml
+except ImportError:
+ YAML_FOUND = False
+ YAML_IMP_ERR = traceback.format_exc()
+else:
+ YAML_FOUND = True
+
+valid_file_format = re.compile(r".*(\.)(yml|yaml|json)$")
+
+
+def ecs_client_argument_spec():
+ return dict(
+ entrust_api_user=dict(type='str', required=True),
+ entrust_api_key=dict(type='str', required=True, no_log=True),
+ entrust_api_client_cert_path=dict(type='path', required=True),
+ entrust_api_client_cert_key_path=dict(type='path', required=True, no_log=True),
+ entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'),
+ )
+
+
+class SessionConfigurationException(Exception):
+ """ Raised if we cannot configure a session with the API """
+
+ pass
+
+
+class RestOperationException(Exception):
+ """ Encapsulate a REST API error """
+
+ def __init__(self, error):
+ self.status = to_native(error.get("status", None))
+ self.errors = [to_native(err.get("message")) for err in error.get("errors", {})]
+ self.message = to_native(" ".join(self.errors))
+
+
+def generate_docstring(operation_spec):
+ """Generate a docstring for an operation defined in operation_spec (swagger)"""
+ # Description of the operation
+ docs = operation_spec.get("description", "No Description")
+ docs += "\n\n"
+
+ # Parameters of the operation
+ parameters = operation_spec.get("parameters", [])
+ if len(parameters) != 0:
+ docs += "\tArguments:\n\n"
+ for parameter in parameters:
+ docs += "{0} ({1}:{2}): {3}\n".format(
+ parameter.get("name"),
+ parameter.get("type", "No Type"),
+ "Required" if parameter.get("required", False) else "Not Required",
+ parameter.get("description"),
+ )
+
+ return docs
+
+
+def bind(instance, method, operation_spec):
+ def binding_scope_fn(*args, **kwargs):
+ return method(instance, *args, **kwargs)
+
+ # Make sure we do not confuse users; add the proper name and documentation to the function.
+ # Users can use !help(<function>) to get help on the function from interactive python or pdb
+ operation_name = operation_spec.get("operationId").split("Using")[0]
+ binding_scope_fn.__name__ = str(operation_name)
+ binding_scope_fn.__doc__ = generate_docstring(operation_spec)
+
+ return binding_scope_fn
+
+
+class RestOperation(object):
+ def __init__(self, session, uri, method, parameters=None):
+ self.session = session
+ self.method = method
+ if parameters is None:
+ self.parameters = {}
+ else:
+ self.parameters = parameters
+ self.url = "{scheme}://{host}{base_path}{uri}".format(scheme="https", host=session._spec.get("host"), base_path=session._spec.get("basePath"), uri=uri)
+
+ def restmethod(self, *args, **kwargs):
+ """Do the hard work of making the request here"""
+
+ # gather named path parameters and do substitution on the URL
+ if self.parameters:
+ path_parameters = {}
+ body_parameters = {}
+ query_parameters = {}
+ for x in self.parameters:
+ expected_location = x.get("in")
+ key_name = x.get("name", None)
+ key_value = kwargs.get(key_name, None)
+ if expected_location == "path" and key_name and key_value:
+ path_parameters.update({key_name: key_value})
+ elif expected_location == "body" and key_name and key_value:
+ body_parameters.update({key_name: key_value})
+ elif expected_location == "query" and key_name and key_value:
+ query_parameters.update({key_name: key_value})
+
+ if len(body_parameters.keys()) >= 1:
+ body_parameters = body_parameters.get(list(body_parameters.keys())[0])
+ else:
+ body_parameters = None
+ else:
+ path_parameters = {}
+ query_parameters = {}
+ body_parameters = None
+
+ # This will fail if we have not set path parameters with a KeyError
+ url = self.url.format(**path_parameters)
+ if query_parameters:
+ # modify the URL to add path parameters
+ url = url + "?" + urlencode(query_parameters)
+
+ try:
+ if body_parameters:
+ body_parameters_json = json.dumps(body_parameters)
+ response = self.session.request.open(method=self.method, url=url, data=body_parameters_json)
+ else:
+ response = self.session.request.open(method=self.method, url=url)
+ request_error = False
+ except HTTPError as e:
+ # An HTTPError has the same methods available as a valid response from request.open
+ response = e
+ request_error = True
+
+ # Return the result if JSON and success ({} for empty responses)
+ # Raise an exception if there was a failure.
+ try:
+ result_code = response.getcode()
+ result = json.loads(response.read())
+ except ValueError:
+ result = {}
+
+ if result or result == {}:
+ if result_code and result_code < 400:
+ return result
+ else:
+ raise RestOperationException(result)
+
+ # Raise a generic RestOperationException if this fails
+ raise RestOperationException({"status": result_code, "errors": [{"message": "REST Operation Failed"}]})
+
+
+class Resource(object):
+ """ Implement basic CRUD operations against a path. """
+
+ def __init__(self, session):
+ self.session = session
+ self.parameters = {}
+
+ for url in session._spec.get("paths").keys():
+ methods = session._spec.get("paths").get(url)
+ for method in methods.keys():
+ operation_spec = methods.get(method)
+ operation_name = operation_spec.get("operationId", None)
+ parameters = operation_spec.get("parameters")
+
+ if not operation_name:
+ if method.lower() == "post":
+ operation_name = "Create"
+ elif method.lower() == "get":
+ operation_name = "Get"
+ elif method.lower() == "put":
+ operation_name = "Update"
+ elif method.lower() == "delete":
+ operation_name = "Delete"
+ elif method.lower() == "patch":
+ operation_name = "Patch"
+ else:
+ raise SessionConfigurationException(to_native("Invalid REST method type {0}".format(method)))
+
+ # Get the non-parameter parts of the URL and append to the operation name
+ # e.g /application/version -> GetApplicationVersion
+ # e.g. /application/{id} -> GetApplication
+ # This may lead to duplicates, which we must prevent.
+ operation_name += re.sub(r"{(.*)}", "", url).replace("/", " ").title().replace(" ", "")
+ operation_spec["operationId"] = operation_name
+
+ op = RestOperation(session, url, method, parameters)
+ setattr(self, operation_name, bind(self, op.restmethod, operation_spec))
+
+
+# Session to encapsulate the connection parameters of the module_utils Request object, the api spec, etc
+class ECSSession(object):
+ def __init__(self, name, **kwargs):
+ """
+ Initialize our session
+ """
+
+ self._set_config(name, **kwargs)
+
+ def client(self):
+ resource = Resource(self)
+ return resource
+
+ def _set_config(self, name, **kwargs):
+ headers = {
+ "Content-Type": "application/json",
+ "Connection": "keep-alive",
+ }
+ self.request = Request(headers=headers, timeout=60)
+
+ configurators = [self._read_config_vars]
+ for configurator in configurators:
+ self._config = configurator(name, **kwargs)
+ if self._config:
+ break
+ if self._config is None:
+ raise SessionConfigurationException(to_native("No Configuration Found."))
+
+ # set up auth if passed
+ entrust_api_user = self.get_config("entrust_api_user")
+ entrust_api_key = self.get_config("entrust_api_key")
+ if entrust_api_user and entrust_api_key:
+ self.request.url_username = entrust_api_user
+ self.request.url_password = entrust_api_key
+ else:
+ raise SessionConfigurationException(to_native("User and key must be provided."))
+
+ # set up client certificate if passed (support all-in one or cert + key)
+ entrust_api_cert = self.get_config("entrust_api_cert")
+ entrust_api_cert_key = self.get_config("entrust_api_cert_key")
+ if entrust_api_cert:
+ self.request.client_cert = entrust_api_cert
+ if entrust_api_cert_key:
+ self.request.client_key = entrust_api_cert_key
+ else:
+ raise SessionConfigurationException(to_native("Client certificate for authentication to the API must be provided."))
+
+ # set up the spec
+ entrust_api_specification_path = self.get_config("entrust_api_specification_path")
+
+ if not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path):
+ raise SessionConfigurationException(to_native("OpenAPI specification was not found at location {0}.".format(entrust_api_specification_path)))
+ if not valid_file_format.match(entrust_api_specification_path):
+ raise SessionConfigurationException(to_native("OpenAPI specification filename must end in .json, .yml or .yaml"))
+
+ self.verify = True
+
+ if entrust_api_specification_path.startswith("http"):
+ try:
+ http_response = Request().open(method="GET", url=entrust_api_specification_path)
+ http_response_contents = http_response.read()
+ if entrust_api_specification_path.endswith(".json"):
+ self._spec = json.load(http_response_contents)
+ elif entrust_api_specification_path.endswith(".yml") or entrust_api_specification_path.endswith(".yaml"):
+ self._spec = yaml.safe_load(http_response_contents)
+ except HTTPError as e:
+ raise SessionConfigurationException(to_native("Error downloading specification from address '{0}', received error code '{1}'".format(
+ entrust_api_specification_path, e.getcode())))
+ else:
+ with open(entrust_api_specification_path) as f:
+ if ".json" in entrust_api_specification_path:
+ self._spec = json.load(f)
+ elif ".yml" in entrust_api_specification_path or ".yaml" in entrust_api_specification_path:
+ self._spec = yaml.safe_load(f)
+
+ def get_config(self, item):
+ return self._config.get(item, None)
+
+ def _read_config_vars(self, name, **kwargs):
+ """ Read configuration from variables passed to the module. """
+ config = {}
+
+ entrust_api_specification_path = kwargs.get("entrust_api_specification_path")
+ if not entrust_api_specification_path or (not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path)):
+ raise SessionConfigurationException(
+ to_native(
+ "Parameter provided for entrust_api_specification_path of value '{0}' was not a valid file path or HTTPS address.".format(
+ entrust_api_specification_path
+ )
+ )
+ )
+
+ for required_file in ["entrust_api_cert", "entrust_api_cert_key"]:
+ file_path = kwargs.get(required_file)
+ if not file_path or not os.path.isfile(file_path):
+ raise SessionConfigurationException(
+ to_native("Parameter provided for {0} of value '{1}' was not a valid file path.".format(required_file, file_path))
+ )
+
+ for required_var in ["entrust_api_user", "entrust_api_key"]:
+ if not kwargs.get(required_var):
+ raise SessionConfigurationException(to_native("Parameter provided for {0} was missing.".format(required_var)))
+
+ config["entrust_api_cert"] = kwargs.get("entrust_api_cert")
+ config["entrust_api_cert_key"] = kwargs.get("entrust_api_cert_key")
+ config["entrust_api_specification_path"] = kwargs.get("entrust_api_specification_path")
+ config["entrust_api_user"] = kwargs.get("entrust_api_user")
+ config["entrust_api_key"] = kwargs.get("entrust_api_key")
+
+ return config
+
+
+def ECSClient(entrust_api_user=None, entrust_api_key=None, entrust_api_cert=None, entrust_api_cert_key=None, entrust_api_specification_path=None):
+ """Create an ECS client"""
+
+ if not YAML_FOUND:
+ raise SessionConfigurationException(missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
+
+ if entrust_api_specification_path is None:
+ entrust_api_specification_path = "https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml"
+
+ # Not functionally necessary with current uses of this module_util, but better to be explicit for future use cases
+ entrust_api_user = to_text(entrust_api_user)
+ entrust_api_key = to_text(entrust_api_key)
+ entrust_api_cert_key = to_text(entrust_api_cert_key)
+ entrust_api_specification_path = to_text(entrust_api_specification_path)
+
+ return ECSSession(
+ "ecs",
+ entrust_api_user=entrust_api_user,
+ entrust_api_key=entrust_api_key,
+ entrust_api_cert=entrust_api_cert,
+ entrust_api_cert_key=entrust_api_cert_key,
+ entrust_api_specification_path=entrust_api_specification_path,
+ ).client()
diff --git a/ansible_collections/community/crypto/plugins/module_utils/io.py b/ansible_collections/community/crypto/plugins/module_utils/io.py
new file mode 100644
index 00000000..6c2f33be
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/io.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import errno
+import os
+import tempfile
+
+
+def load_file(path, module=None):
+ '''
+ Load the file as a bytes string.
+ '''
+ try:
+ with open(path, 'rb') as f:
+ return f.read()
+ except Exception as exc:
+ if module is None:
+ raise
+ module.fail_json('Error while loading {0} - {1}'.format(path, str(exc)))
+
+
+def load_file_if_exists(path, module=None, ignore_errors=False):
+ '''
+ Load the file as a bytes string. If the file does not exist, ``None`` is returned.
+
+ If ``ignore_errors`` is ``True``, will ignore errors. Otherwise, errors are
+ raised as exceptions if ``module`` is not specified, and result in ``module.fail_json``
+ being called when ``module`` is specified.
+ '''
+ try:
+ with open(path, 'rb') as f:
+ return f.read()
+ except EnvironmentError as exc:
+ if exc.errno == errno.ENOENT:
+ return None
+ if ignore_errors:
+ return None
+ if module is None:
+ raise
+ module.fail_json('Error while loading {0} - {1}'.format(path, str(exc)))
+ except Exception as exc:
+ if ignore_errors:
+ return None
+ if module is None:
+ raise
+ module.fail_json('Error while loading {0} - {1}'.format(path, str(exc)))
+
+
+def write_file(module, content, default_mode=None, path=None):
+ '''
+ Writes content into destination file as securely as possible.
+ Uses file arguments from module.
+ '''
+ # Find out parameters for file
+ try:
+ file_args = module.load_file_common_arguments(module.params, path=path)
+ except TypeError:
+ # The path argument is only supported in Ansible 2.10+. Fall back to
+ # pre-2.10 behavior of module_utils/crypto.py for older Ansible versions.
+ file_args = module.load_file_common_arguments(module.params)
+ if path is not None:
+ file_args['path'] = path
+ if file_args['mode'] is None:
+ file_args['mode'] = default_mode
+ # Create tempfile name
+ tmp_fd, tmp_name = tempfile.mkstemp(prefix=b'.ansible_tmp')
+ try:
+ os.close(tmp_fd)
+ except Exception:
+ pass
+ module.add_cleanup_file(tmp_name) # if we fail, let Ansible try to remove the file
+ try:
+ try:
+ # Create tempfile
+ file = os.open(tmp_name, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
+ os.write(file, content)
+ os.close(file)
+ except Exception as e:
+ try:
+ os.remove(tmp_name)
+ except Exception:
+ pass
+ module.fail_json(msg='Error while writing result into temporary file: {0}'.format(e))
+ # Update destination to wanted permissions
+ if os.path.exists(file_args['path']):
+ module.set_fs_attributes_if_different(file_args, False)
+ # Move tempfile to final destination
+ module.atomic_move(tmp_name, file_args['path'])
+ # Try to update permissions again
+ if not module.check_file_absent_if_check_mode(file_args['path']):
+ module.set_fs_attributes_if_different(file_args, False)
+ except Exception as e:
+ try:
+ os.remove(tmp_name)
+ except Exception:
+ pass
+ module.fail_json(msg='Error while writing result: {0}'.format(e))
diff --git a/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/common.py b/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/common.py
new file mode 100644
index 00000000..6e274a6d
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/common.py
@@ -0,0 +1,346 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import abc
+import os
+import stat
+import traceback
+
+from ansible.module_utils import six
+
+from ansible.module_utils.common.text.converters import to_native
+from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
+ parse_openssh_version,
+)
+
+
+def restore_on_failure(f):
+ def backup_and_restore(module, path, *args, **kwargs):
+ backup_file = module.backup_local(path) if os.path.exists(path) else None
+
+ try:
+ f(module, path, *args, **kwargs)
+ except Exception:
+ if backup_file is not None:
+ module.atomic_move(backup_file, path)
+ raise
+ else:
+ module.add_cleanup_file(backup_file)
+
+ return backup_and_restore
+
+
+@restore_on_failure
+def safe_atomic_move(module, path, destination):
+ module.atomic_move(path, destination)
+
+
+def _restore_all_on_failure(f):
+ def backup_and_restore(self, sources_and_destinations, *args, **kwargs):
+ backups = [(d, self.module.backup_local(d)) for s, d in sources_and_destinations if os.path.exists(d)]
+
+ try:
+ f(self, sources_and_destinations, *args, **kwargs)
+ except Exception:
+ for destination, backup in backups:
+ self.module.atomic_move(backup, destination)
+ raise
+ else:
+ for destination, backup in backups:
+ self.module.add_cleanup_file(backup)
+ return backup_and_restore
+
+
+@six.add_metaclass(abc.ABCMeta)
+class OpensshModule(object):
+ def __init__(self, module):
+ self.module = module
+
+ self.changed = False
+ self.check_mode = self.module.check_mode
+
+ def execute(self):
+ try:
+ self._execute()
+ except Exception as e:
+ self.module.fail_json(
+ msg="unexpected error occurred: %s" % to_native(e),
+ exception=traceback.format_exc(),
+ )
+
+ self.module.exit_json(**self.result)
+
+ @abc.abstractmethod
+ def _execute(self):
+ pass
+
+ @property
+ def result(self):
+ result = self._result
+
+ result['changed'] = self.changed
+
+ if self.module._diff:
+ result['diff'] = self.diff
+
+ return result
+
+ @property
+ @abc.abstractmethod
+ def _result(self):
+ pass
+
+ @property
+ @abc.abstractmethod
+ def diff(self):
+ pass
+
+ @staticmethod
+ def skip_if_check_mode(f):
+ def wrapper(self, *args, **kwargs):
+ if not self.check_mode:
+ f(self, *args, **kwargs)
+ return wrapper
+
+ @staticmethod
+ def trigger_change(f):
+ def wrapper(self, *args, **kwargs):
+ f(self, *args, **kwargs)
+ self.changed = True
+ return wrapper
+
+ def _check_if_base_dir(self, path):
+ base_dir = os.path.dirname(path) or '.'
+ if not os.path.isdir(base_dir):
+ self.module.fail_json(
+ name=base_dir,
+ msg='The directory %s does not exist or the file is not a directory' % base_dir
+ )
+
+ def _get_ssh_version(self):
+ ssh_bin = self.module.get_bin_path('ssh')
+ if not ssh_bin:
+ return ""
+ return parse_openssh_version(self.module.run_command([ssh_bin, '-V', '-q'])[2].strip())
+
+ @_restore_all_on_failure
+ def _safe_secure_move(self, sources_and_destinations):
+ """Moves a list of files from 'source' to 'destination' and restores 'destination' from backup upon failure.
+ If 'destination' does not already exist, then 'source' permissions are preserved to prevent
+ exposing protected data ('atomic_move' uses the 'destination' base directory mask for
+ permissions if 'destination' does not already exists).
+ """
+ for source, destination in sources_and_destinations:
+ if os.path.exists(destination):
+ self.module.atomic_move(source, destination)
+ else:
+ self.module.preserved_copy(source, destination)
+
+ def _update_permissions(self, path):
+ file_args = self.module.load_file_common_arguments(self.module.params)
+ file_args['path'] = path
+
+ if not self.module.check_file_absent_if_check_mode(path):
+ self.changed = self.module.set_fs_attributes_if_different(file_args, self.changed)
+ else:
+ self.changed = True
+
+
+class KeygenCommand(object):
+ def __init__(self, module):
+ self._bin_path = module.get_bin_path('ssh-keygen', True)
+ self._run_command = module.run_command
+
+ def generate_certificate(self, certificate_path, identifier, options, pkcs11_provider, principals,
+ serial_number, signature_algorithm, signing_key_path, type,
+ time_parameters, use_agent, **kwargs):
+ args = [self._bin_path, '-s', signing_key_path, '-P', '', '-I', identifier]
+
+ if options:
+ for option in options:
+ args.extend(['-O', option])
+ if pkcs11_provider:
+ args.extend(['-D', pkcs11_provider])
+ if principals:
+ args.extend(['-n', ','.join(principals)])
+ if serial_number is not None:
+ args.extend(['-z', str(serial_number)])
+ if type == 'host':
+ args.extend(['-h'])
+ if use_agent:
+ args.extend(['-U'])
+ if time_parameters.validity_string:
+ args.extend(['-V', time_parameters.validity_string])
+ if signature_algorithm:
+ args.extend(['-t', signature_algorithm])
+ args.append(certificate_path)
+
+ return self._run_command(args, **kwargs)
+
+ def generate_keypair(self, private_key_path, size, type, comment, **kwargs):
+ args = [
+ self._bin_path,
+ '-q',
+ '-N', '',
+ '-b', str(size),
+ '-t', type,
+ '-f', private_key_path,
+ '-C', comment or ''
+ ]
+
+ # "y" must be entered in response to the "overwrite" prompt
+ data = 'y' if os.path.exists(private_key_path) else None
+
+ return self._run_command(args, data=data, **kwargs)
+
+ def get_certificate_info(self, certificate_path, **kwargs):
+ return self._run_command([self._bin_path, '-L', '-f', certificate_path], **kwargs)
+
+ def get_matching_public_key(self, private_key_path, **kwargs):
+ return self._run_command([self._bin_path, '-P', '', '-y', '-f', private_key_path], **kwargs)
+
+ def get_private_key(self, private_key_path, **kwargs):
+ return self._run_command([self._bin_path, '-l', '-f', private_key_path], **kwargs)
+
+ def update_comment(self, private_key_path, comment, **kwargs):
+ if os.path.exists(private_key_path) and not os.access(private_key_path, os.W_OK):
+ try:
+ os.chmod(private_key_path, stat.S_IWUSR + stat.S_IRUSR)
+ except (IOError, OSError) as e:
+ raise e("The private key at %s is not writeable preventing a comment update" % private_key_path)
+
+ return self._run_command([self._bin_path, '-q', '-o', '-c', '-C', comment, '-f', private_key_path], **kwargs)
+
+
+class PrivateKey(object):
+ def __init__(self, size, key_type, fingerprint, format=''):
+ self._size = size
+ self._type = key_type
+ self._fingerprint = fingerprint
+ self._format = format
+
+ @property
+ def size(self):
+ return self._size
+
+ @property
+ def type(self):
+ return self._type
+
+ @property
+ def fingerprint(self):
+ return self._fingerprint
+
+ @property
+ def format(self):
+ return self._format
+
+ @classmethod
+ def from_string(cls, string):
+ properties = string.split()
+
+ return cls(
+ size=int(properties[0]),
+ key_type=properties[-1][1:-1].lower(),
+ fingerprint=properties[1],
+ )
+
+ def to_dict(self):
+ return {
+ 'size': self._size,
+ 'type': self._type,
+ 'fingerprint': self._fingerprint,
+ 'format': self._format,
+ }
+
+
+class PublicKey(object):
+ def __init__(self, type_string, data, comment):
+ self._type_string = type_string
+ self._data = data
+ self._comment = comment
+
+ def __eq__(self, other):
+ if not isinstance(other, type(self)):
+ return NotImplemented
+
+ return all([
+ self._type_string == other._type_string,
+ self._data == other._data,
+ (self._comment == other._comment) if self._comment is not None and other._comment is not None else True
+ ])
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __str__(self):
+ return "%s %s" % (self._type_string, self._data)
+
+ @property
+ def comment(self):
+ return self._comment
+
+ @comment.setter
+ def comment(self, value):
+ self._comment = value
+
+ @property
+ def data(self):
+ return self._data
+
+ @property
+ def type_string(self):
+ return self._type_string
+
+ @classmethod
+ def from_string(cls, string):
+ properties = string.strip('\n').split(' ', 2)
+
+ return cls(
+ type_string=properties[0],
+ data=properties[1],
+ comment=properties[2] if len(properties) > 2 else ""
+ )
+
+ @classmethod
+ def load(cls, path):
+ try:
+ with open(path, 'r') as f:
+ properties = f.read().strip(' \n').split(' ', 2)
+ except (IOError, OSError):
+ raise
+
+ if len(properties) < 2:
+ return None
+
+ return cls(
+ type_string=properties[0],
+ data=properties[1],
+ comment='' if len(properties) <= 2 else properties[2],
+ )
+
+ def to_dict(self):
+ return {
+ 'comment': self._comment,
+ 'public_key': self._data,
+ }
+
+
+def parse_private_key_format(path):
+ with open(path, 'r') as file:
+ header = file.readline().strip()
+
+ if header == '-----BEGIN OPENSSH PRIVATE KEY-----':
+ return 'SSH'
+ elif header == '-----BEGIN PRIVATE KEY-----':
+ return 'PKCS8'
+ elif header == '-----BEGIN RSA PRIVATE KEY-----':
+ return 'PKCS1'
+
+ return ''
diff --git a/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/keypair_backend.py b/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/keypair_backend.py
new file mode 100644
index 00000000..8cc39c6f
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/openssh/backends/keypair_backend.py
@@ -0,0 +1,480 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at>
+# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import abc
+import os
+
+from ansible.module_utils import six
+from ansible.module_utils.basic import missing_required_lib
+from ansible.module_utils.common.text.converters import to_native, to_text, to_bytes
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.openssh.cryptography import (
+ HAS_OPENSSH_SUPPORT,
+ HAS_OPENSSH_PRIVATE_FORMAT,
+ InvalidCommentError,
+ InvalidPassphraseError,
+ InvalidPrivateKeyFileError,
+ OpenSSHError,
+ OpensshKeypair,
+)
+from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import (
+ KeygenCommand,
+ OpensshModule,
+ PrivateKey,
+ PublicKey,
+ parse_private_key_format,
+)
+from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
+ any_in,
+ file_mode,
+ secure_write,
+)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class KeypairBackend(OpensshModule):
+
+ def __init__(self, module):
+ super(KeypairBackend, self).__init__(module)
+
+ self.comment = self.module.params['comment']
+ self.private_key_path = self.module.params['path']
+ self.public_key_path = self.private_key_path + '.pub'
+ self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always'
+ self.state = self.module.params['state']
+ self.type = self.module.params['type']
+
+ self.size = self._get_size(self.module.params['size'])
+ self._validate_path()
+
+ self.original_private_key = None
+ self.original_public_key = None
+ self.private_key = None
+ self.public_key = None
+
+ def _get_size(self, size):
+ if self.type in ('rsa', 'rsa1'):
+ result = 4096 if size is None else size
+ if result < 1024:
+ return self.module.fail_json(
+ msg="For RSA keys, the minimum size is 1024 bits and the default is 4096 bits. " +
+ "Attempting to use bit lengths under 1024 will cause the module to fail."
+ )
+ elif self.type == 'dsa':
+ result = 1024 if size is None else size
+ if result != 1024:
+ return self.module.fail_json(msg="DSA keys must be exactly 1024 bits as specified by FIPS 186-2.")
+ elif self.type == 'ecdsa':
+ result = 256 if size is None else size
+ if result not in (256, 384, 521):
+ return self.module.fail_json(
+ msg="For ECDSA keys, size determines the key length by selecting from one of " +
+ "three elliptic curve sizes: 256, 384 or 521 bits. " +
+ "Attempting to use bit lengths other than these three values for ECDSA keys will " +
+ "cause this module to fail."
+ )
+ elif self.type == 'ed25519':
+ # User input is ignored for `key size` when `key type` is ed25519
+ result = 256
+ else:
+ return self.module.fail_json(msg="%s is not a valid value for key type" % self.type)
+
+ return result
+
+ def _validate_path(self):
+ self._check_if_base_dir(self.private_key_path)
+
+ if os.path.isdir(self.private_key_path):
+ self.module.fail_json(msg='%s is a directory. Please specify a path to a file.' % self.private_key_path)
+
+ def _execute(self):
+ self.original_private_key = self._load_private_key()
+ self.original_public_key = self._load_public_key()
+
+ if self.state == 'present':
+ self._validate_key_load()
+
+ if self._should_generate():
+ self._generate()
+ elif not self._public_key_valid():
+ self._restore_public_key()
+
+ self.private_key = self._load_private_key()
+ self.public_key = self._load_public_key()
+
+ for path in (self.private_key_path, self.public_key_path):
+ self._update_permissions(path)
+ else:
+ if self._should_remove():
+ self._remove()
+
+ def _load_private_key(self):
+ result = None
+ if self._private_key_exists():
+ try:
+ result = self._get_private_key()
+ except Exception:
+ pass
+
+ return result
+
+ def _private_key_exists(self):
+ return os.path.exists(self.private_key_path)
+
+ @abc.abstractmethod
+ def _get_private_key(self):
+ pass
+
+ def _load_public_key(self):
+ result = None
+ if self._public_key_exists():
+ try:
+ result = PublicKey.load(self.public_key_path)
+ except (IOError, OSError):
+ pass
+ return result
+
+ def _public_key_exists(self):
+ return os.path.exists(self.public_key_path)
+
+ def _validate_key_load(self):
+ if (self._private_key_exists()
+ and self.regenerate in ('never', 'fail', 'partial_idempotence')
+ and (self.original_private_key is None or not self._private_key_readable())):
+ self.module.fail_json(
+ msg="Unable to read the key. The key is protected with a passphrase or broken. " +
+ "Will not proceed. To force regeneration, call the module with `generate` " +
+ "set to `full_idempotence` or `always`, or with `force=true`."
+ )
+
+ @abc.abstractmethod
+ def _private_key_readable(self):
+ pass
+
+ def _should_generate(self):
+ if self.regenerate == 'never':
+ return self.original_private_key is None
+ elif self.regenerate == 'fail':
+ if not self._private_key_valid():
+ self.module.fail_json(
+ msg="Key has wrong type and/or size. Will not proceed. " +
+ "To force regeneration, call the module with `generate` set to " +
+ "`partial_idempotence`, `full_idempotence` or `always`, or with `force=true`."
+ )
+ return self.original_private_key is None
+ elif self.regenerate in ('partial_idempotence', 'full_idempotence'):
+ return not self._private_key_valid()
+ else:
+ return True
+
+ def _private_key_valid(self):
+ if self.original_private_key is None:
+ return False
+
+ return all([
+ self.size == self.original_private_key.size,
+ self.type == self.original_private_key.type,
+ self._private_key_valid_backend(),
+ ])
+
+ @abc.abstractmethod
+ def _private_key_valid_backend(self):
+ pass
+
+ @OpensshModule.trigger_change
+ @OpensshModule.skip_if_check_mode
+ def _generate(self):
+ temp_private_key, temp_public_key = self._generate_temp_keypair()
+
+ try:
+ self._safe_secure_move([(temp_private_key, self.private_key_path), (temp_public_key, self.public_key_path)])
+ except OSError as e:
+ self.module.fail_json(msg=to_native(e))
+
+ def _generate_temp_keypair(self):
+ temp_private_key = os.path.join(self.module.tmpdir, os.path.basename(self.private_key_path))
+ temp_public_key = temp_private_key + '.pub'
+
+ try:
+ self._generate_keypair(temp_private_key)
+ except (IOError, OSError) as e:
+ self.module.fail_json(msg=to_native(e))
+
+ for f in (temp_private_key, temp_public_key):
+ self.module.add_cleanup_file(f)
+
+ return temp_private_key, temp_public_key
+
+ @abc.abstractmethod
+ def _generate_keypair(self, private_key_path):
+ pass
+
+ def _public_key_valid(self):
+ if self.original_public_key is None:
+ return False
+
+ valid_public_key = self._get_public_key()
+ valid_public_key.comment = self.comment
+
+ return self.original_public_key == valid_public_key
+
+ @abc.abstractmethod
+ def _get_public_key(self):
+ pass
+
+ @OpensshModule.trigger_change
+ @OpensshModule.skip_if_check_mode
+ def _restore_public_key(self):
+ try:
+ temp_public_key = self._create_temp_public_key(str(self._get_public_key()) + '\n')
+ self._safe_secure_move([
+ (temp_public_key, self.public_key_path)
+ ])
+ except (IOError, OSError):
+ self.module.fail_json(
+ msg="The public key is missing or does not match the private key. " +
+ "Unable to regenerate the public key."
+ )
+
+ if self.comment:
+ self._update_comment()
+
+ def _create_temp_public_key(self, content):
+ temp_public_key = os.path.join(self.module.tmpdir, os.path.basename(self.public_key_path))
+
+ default_permissions = 0o644
+ existing_permissions = file_mode(self.public_key_path)
+
+ try:
+ secure_write(temp_public_key, existing_permissions or default_permissions, to_bytes(content))
+ except (IOError, OSError) as e:
+ self.module.fail_json(msg=to_native(e))
+ self.module.add_cleanup_file(temp_public_key)
+
+ return temp_public_key
+
+ @abc.abstractmethod
+ def _update_comment(self):
+ pass
+
+ def _should_remove(self):
+ return self._private_key_exists() or self._public_key_exists()
+
+ @OpensshModule.trigger_change
+ @OpensshModule.skip_if_check_mode
+ def _remove(self):
+ try:
+ if self._private_key_exists():
+ os.remove(self.private_key_path)
+ if self._public_key_exists():
+ os.remove(self.public_key_path)
+ except (IOError, OSError) as e:
+ self.module.fail_json(msg=to_native(e))
+
+ @property
+ def _result(self):
+ private_key = self.private_key or self.original_private_key
+ public_key = self.public_key or self.original_public_key
+
+ return {
+ 'size': self.size,
+ 'type': self.type,
+ 'filename': self.private_key_path,
+ 'fingerprint': private_key.fingerprint if private_key else '',
+ 'public_key': str(public_key) if public_key else '',
+ 'comment': public_key.comment if public_key else '',
+ }
+
+ @property
+ def diff(self):
+ before = self.original_private_key.to_dict() if self.original_private_key else {}
+ before.update(self.original_public_key.to_dict() if self.original_public_key else {})
+
+ after = self.private_key.to_dict() if self.private_key else {}
+ after.update(self.public_key.to_dict() if self.public_key else {})
+
+ return {
+ 'before': before,
+ 'after': after,
+ }
+
+
+class KeypairBackendOpensshBin(KeypairBackend):
+ def __init__(self, module):
+ super(KeypairBackendOpensshBin, self).__init__(module)
+
+ if self.module.params['private_key_format'] != 'auto':
+ self.module.fail_json(
+ msg="'auto' is the only valid option for " +
+ "'private_key_format' when 'backend' is not 'cryptography'"
+ )
+
+ self.ssh_keygen = KeygenCommand(self.module)
+
+ def _generate_keypair(self, private_key_path):
+ self.ssh_keygen.generate_keypair(private_key_path, self.size, self.type, self.comment)
+
+ def _get_private_key(self):
+ private_key_content = self.ssh_keygen.get_private_key(self.private_key_path)[1]
+ return PrivateKey.from_string(private_key_content)
+
+ def _get_public_key(self):
+ public_key_content = self.ssh_keygen.get_matching_public_key(self.private_key_path)[1]
+ return PublicKey.from_string(public_key_content)
+
+ def _private_key_readable(self):
+ rc, stdout, stderr = self.ssh_keygen.get_matching_public_key(self.private_key_path)
+ return not (rc == 255 or any_in(stderr, 'is not a public key file', 'incorrect passphrase', 'load failed'))
+
+ def _update_comment(self):
+ try:
+ self.ssh_keygen.update_comment(self.private_key_path, self.comment)
+ except (IOError, OSError) as e:
+ self.module.fail_json(msg=to_native(e))
+
+ def _private_key_valid_backend(self):
+ return True
+
+
+class KeypairBackendCryptography(KeypairBackend):
+ def __init__(self, module):
+ super(KeypairBackendCryptography, self).__init__(module)
+
+ if self.type == 'rsa1':
+ self.module.fail_json(msg="RSA1 keys are not supported by the cryptography backend")
+
+ self.passphrase = to_bytes(module.params['passphrase']) if module.params['passphrase'] else None
+ self.private_key_format = self._get_key_format(module.params['private_key_format'])
+
+ def _get_key_format(self, key_format):
+ result = 'SSH'
+
+ if key_format == 'auto':
+ # Default to OpenSSH 7.8 compatibility when OpenSSH is not installed
+ ssh_version = self._get_ssh_version() or "7.8"
+
+ if LooseVersion(ssh_version) < LooseVersion("7.8") and self.type != 'ed25519':
+ # OpenSSH made SSH formatted private keys available in version 6.5,
+ # but still defaulted to PKCS1 format with the exception of ed25519 keys
+ result = 'PKCS1'
+
+ if result == 'SSH' and not HAS_OPENSSH_PRIVATE_FORMAT:
+ self.module.fail_json(
+ msg=missing_required_lib(
+ 'cryptography >= 3.0',
+ reason="to load/dump private keys in the default OpenSSH format for OpenSSH >= 7.8 " +
+ "or for ed25519 keys"
+ )
+ )
+ else:
+ result = key_format.upper()
+
+ return result
+
+ def _generate_keypair(self, private_key_path):
+ keypair = OpensshKeypair.generate(
+ keytype=self.type,
+ size=self.size,
+ passphrase=self.passphrase,
+ comment=self.comment or '',
+ )
+
+ encoded_private_key = OpensshKeypair.encode_openssh_privatekey(
+ keypair.asymmetric_keypair, self.private_key_format
+ )
+ secure_write(private_key_path, 0o600, encoded_private_key)
+
+ public_key_path = private_key_path + '.pub'
+ secure_write(public_key_path, 0o644, keypair.public_key)
+
+ def _get_private_key(self):
+ keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
+
+ return PrivateKey(
+ size=keypair.size,
+ key_type=keypair.key_type,
+ fingerprint=keypair.fingerprint,
+ format=parse_private_key_format(self.private_key_path)
+ )
+
+ def _get_public_key(self):
+ try:
+ keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
+ except OpenSSHError:
+ # Simulates the null output of ssh-keygen
+ return ""
+
+ return PublicKey.from_string(to_text(keypair.public_key))
+
+ def _private_key_readable(self):
+ try:
+ OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
+ except (InvalidPrivateKeyFileError, InvalidPassphraseError):
+ return False
+
+ # Cryptography >= 3.0 uses a SSH key loader which does not raise an exception when a passphrase is provided
+ # when loading an unencrypted key
+ if self.passphrase:
+ try:
+ OpensshKeypair.load(path=self.private_key_path, passphrase=None, no_public_key=True)
+ except (InvalidPrivateKeyFileError, InvalidPassphraseError):
+ return True
+ else:
+ return False
+
+ return True
+
+ def _update_comment(self):
+ keypair = OpensshKeypair.load(path=self.private_key_path, passphrase=self.passphrase, no_public_key=True)
+ try:
+ keypair.comment = self.comment
+ except InvalidCommentError as e:
+ self.module.fail_json(msg=to_native(e))
+
+ try:
+ temp_public_key = self._create_temp_public_key(keypair.public_key + b'\n')
+ self._safe_secure_move([(temp_public_key, self.public_key_path)])
+ except (IOError, OSError) as e:
+ self.module.fail_json(msg=to_native(e))
+
+ def _private_key_valid_backend(self):
+ # avoids breaking behavior and prevents
+ # automatic conversions with OpenSSH upgrades
+ if self.module.params['private_key_format'] == 'auto':
+ return True
+
+ return self.private_key_format == self.original_private_key.format
+
+
+def select_backend(module, backend):
+ can_use_cryptography = HAS_OPENSSH_SUPPORT
+ can_use_opensshbin = bool(module.get_bin_path('ssh-keygen'))
+
+ if backend == 'auto':
+ if can_use_opensshbin and not module.params['passphrase']:
+ backend = 'opensshbin'
+ elif can_use_cryptography:
+ backend = 'cryptography'
+ else:
+ module.fail_json(msg="Cannot find either the OpenSSH binary in the PATH " +
+ "or cryptography >= 2.6 installed on this system")
+
+ if backend == 'opensshbin':
+ if not can_use_opensshbin:
+ module.fail_json(msg="Cannot find the OpenSSH binary in the PATH")
+ return backend, KeypairBackendOpensshBin(module)
+ elif backend == 'cryptography':
+ if not can_use_cryptography:
+ module.fail_json(msg=missing_required_lib("cryptography >= 2.6"))
+ return backend, KeypairBackendCryptography(module)
+ else:
+ raise ValueError('Unsupported value for backend: {0}'.format(backend))
diff --git a/ansible_collections/community/crypto/plugins/module_utils/openssh/certificate.py b/ansible_collections/community/crypto/plugins/module_utils/openssh/certificate.py
new file mode 100644
index 00000000..54d1b1ec
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/openssh/certificate.py
@@ -0,0 +1,666 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+# Protocol References
+# -------------------
+# https://datatracker.ietf.org/doc/html/rfc4251
+# https://datatracker.ietf.org/doc/html/rfc4253
+# https://datatracker.ietf.org/doc/html/rfc5656
+# https://datatracker.ietf.org/doc/html/rfc8032
+# https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
+#
+# Inspired by:
+# ------------
+# https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/serialization/ssh.py
+# https://github.com/paramiko/paramiko/blob/master/paramiko/message.py
+
+import abc
+import binascii
+import os
+from base64 import b64encode
+from datetime import datetime
+from hashlib import sha256
+
+from ansible.module_utils import six
+from ansible.module_utils.common.text.converters import to_text
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import convert_relative_to_datetime
+from ansible_collections.community.crypto.plugins.module_utils.openssh.utils import (
+ OpensshParser,
+ _OpensshWriter,
+)
+
+# See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
+_USER_TYPE = 1
+_HOST_TYPE = 2
+
+_SSH_TYPE_STRINGS = {
+ 'rsa': b"ssh-rsa",
+ 'dsa': b"ssh-dss",
+ 'ecdsa-nistp256': b"ecdsa-sha2-nistp256",
+ 'ecdsa-nistp384': b"ecdsa-sha2-nistp384",
+ 'ecdsa-nistp521': b"ecdsa-sha2-nistp521",
+ 'ed25519': b"ssh-ed25519",
+}
+_CERT_SUFFIX_V01 = b"-cert-v01@openssh.com"
+
+# See https://datatracker.ietf.org/doc/html/rfc5656#section-6.1
+_ECDSA_CURVE_IDENTIFIERS = {
+ 'ecdsa-nistp256': b'nistp256',
+ 'ecdsa-nistp384': b'nistp384',
+ 'ecdsa-nistp521': b'nistp521',
+}
+_ECDSA_CURVE_IDENTIFIERS_LOOKUP = {
+ b'nistp256': 'ecdsa-nistp256',
+ b'nistp384': 'ecdsa-nistp384',
+ b'nistp521': 'ecdsa-nistp521',
+}
+
+_ALWAYS = datetime(1970, 1, 1)
+_FOREVER = datetime.max
+
+_CRITICAL_OPTIONS = (
+ 'force-command',
+ 'source-address',
+ 'verify-required',
+)
+
+_DIRECTIVES = (
+ 'clear',
+ 'no-x11-forwarding',
+ 'no-agent-forwarding',
+ 'no-port-forwarding',
+ 'no-pty',
+ 'no-user-rc',
+)
+
+_EXTENSIONS = (
+ 'permit-x11-forwarding',
+ 'permit-agent-forwarding',
+ 'permit-port-forwarding',
+ 'permit-pty',
+ 'permit-user-rc'
+)
+
+if six.PY3:
+ long = int
+
+
+class OpensshCertificateTimeParameters(object):
+ def __init__(self, valid_from, valid_to):
+ self._valid_from = self.to_datetime(valid_from)
+ self._valid_to = self.to_datetime(valid_to)
+
+ if self._valid_from > self._valid_to:
+ raise ValueError("Valid from: %s must not be greater than Valid to: %s" % (valid_from, valid_to))
+
+ def __eq__(self, other):
+ if not isinstance(other, type(self)):
+ return NotImplemented
+ else:
+ return self._valid_from == other._valid_from and self._valid_to == other._valid_to
+
+ def __ne__(self, other):
+ return not self == other
+
+ @property
+ def validity_string(self):
+ if not (self._valid_from == _ALWAYS and self._valid_to == _FOREVER):
+ return "%s:%s" % (
+ self.valid_from(date_format='openssh'), self.valid_to(date_format='openssh')
+ )
+ return ""
+
+ def valid_from(self, date_format):
+ return self.format_datetime(self._valid_from, date_format)
+
+ def valid_to(self, date_format):
+ return self.format_datetime(self._valid_to, date_format)
+
+ def within_range(self, valid_at):
+ if valid_at is not None:
+ valid_at_datetime = self.to_datetime(valid_at)
+ return self._valid_from <= valid_at_datetime <= self._valid_to
+ return True
+
+ @staticmethod
+ def format_datetime(dt, date_format):
+ if date_format in ('human_readable', 'openssh'):
+ if dt == _ALWAYS:
+ result = 'always'
+ elif dt == _FOREVER:
+ result = 'forever'
+ else:
+ result = dt.isoformat() if date_format == 'human_readable' else dt.strftime("%Y%m%d%H%M%S")
+ elif date_format == 'timestamp':
+ td = dt - _ALWAYS
+ result = int((td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6)
+ else:
+ raise ValueError("%s is not a valid format" % date_format)
+ return result
+
+ @staticmethod
+ def to_datetime(time_string_or_timestamp):
+ try:
+ if isinstance(time_string_or_timestamp, six.string_types):
+ result = OpensshCertificateTimeParameters._time_string_to_datetime(time_string_or_timestamp.strip())
+ elif isinstance(time_string_or_timestamp, (long, int)):
+ result = OpensshCertificateTimeParameters._timestamp_to_datetime(time_string_or_timestamp)
+ else:
+ raise ValueError(
+ "Value must be of type (str, unicode, int, long) not %s" % type(time_string_or_timestamp)
+ )
+ except ValueError:
+ raise
+ return result
+
+ @staticmethod
+ def _timestamp_to_datetime(timestamp):
+ if timestamp == 0x0:
+ result = _ALWAYS
+ elif timestamp == 0xFFFFFFFFFFFFFFFF:
+ result = _FOREVER
+ else:
+ try:
+ result = datetime.utcfromtimestamp(timestamp)
+ except OverflowError as e:
+ raise ValueError
+ return result
+
+ @staticmethod
+ def _time_string_to_datetime(time_string):
+ result = None
+ if time_string == 'always':
+ result = _ALWAYS
+ elif time_string == 'forever':
+ result = _FOREVER
+ elif is_relative_time_string(time_string):
+ result = convert_relative_to_datetime(time_string)
+ else:
+ for time_format in ("%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"):
+ try:
+ result = datetime.strptime(time_string, time_format)
+ except ValueError:
+ pass
+ if result is None:
+ raise ValueError
+ return result
+
+
+class OpensshCertificateOption(object):
+ def __init__(self, option_type, name, data):
+ if option_type not in ('critical', 'extension'):
+ raise ValueError("type must be either 'critical' or 'extension'")
+
+ if not isinstance(name, six.string_types):
+ raise TypeError("name must be a string not %s" % type(name))
+
+ if not isinstance(data, six.string_types):
+ raise TypeError("data must be a string not %s" % type(data))
+
+ self._option_type = option_type
+ self._name = name.lower()
+ self._data = data
+
+ def __eq__(self, other):
+ if not isinstance(other, type(self)):
+ return NotImplemented
+
+ return all([
+ self._option_type == other._option_type,
+ self._name == other._name,
+ self._data == other._data,
+ ])
+
+ def __hash__(self):
+ return hash((self._option_type, self._name, self._data))
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __str__(self):
+ if self._data:
+ return "%s=%s" % (self._name, self._data)
+ return self._name
+
+ @property
+ def data(self):
+ return self._data
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def type(self):
+ return self._option_type
+
+ @classmethod
+ def from_string(cls, option_string):
+ if not isinstance(option_string, six.string_types):
+ raise ValueError("option_string must be a string not %s" % type(option_string))
+ option_type = None
+
+ if ':' in option_string:
+ option_type, value = option_string.strip().split(':', 1)
+ if '=' in value:
+ name, data = value.split('=', 1)
+ else:
+ name, data = value, ''
+ elif '=' in option_string:
+ name, data = option_string.strip().split('=', 1)
+ else:
+ name, data = option_string.strip(), ''
+
+ return cls(
+ option_type=option_type or get_option_type(name.lower()),
+ name=name,
+ data=data
+ )
+
+
+@six.add_metaclass(abc.ABCMeta)
+class OpensshCertificateInfo:
+ """Encapsulates all certificate information which is signed by a CA key"""
+ def __init__(self,
+ nonce=None,
+ serial=None,
+ cert_type=None,
+ key_id=None,
+ principals=None,
+ valid_after=None,
+ valid_before=None,
+ critical_options=None,
+ extensions=None,
+ reserved=None,
+ signing_key=None):
+ self.nonce = nonce
+ self.serial = serial
+ self._cert_type = cert_type
+ self.key_id = key_id
+ self.principals = principals
+ self.valid_after = valid_after
+ self.valid_before = valid_before
+ self.critical_options = critical_options
+ self.extensions = extensions
+ self.reserved = reserved
+ self.signing_key = signing_key
+
+ self.type_string = None
+
+ @property
+ def cert_type(self):
+ if self._cert_type == _USER_TYPE:
+ return 'user'
+ elif self._cert_type == _HOST_TYPE:
+ return 'host'
+ else:
+ return ''
+
+ @cert_type.setter
+ def cert_type(self, cert_type):
+ if cert_type == 'user' or cert_type == _USER_TYPE:
+ self._cert_type = _USER_TYPE
+ elif cert_type == 'host' or cert_type == _HOST_TYPE:
+ self._cert_type = _HOST_TYPE
+ else:
+ raise ValueError("%s is not a valid certificate type" % cert_type)
+
+ def signing_key_fingerprint(self):
+ return fingerprint(self.signing_key)
+
+ @abc.abstractmethod
+ def public_key_fingerprint(self):
+ pass
+
+ @abc.abstractmethod
+ def parse_public_numbers(self, parser):
+ pass
+
+
+class OpensshRSACertificateInfo(OpensshCertificateInfo):
+ def __init__(self, e=None, n=None, **kwargs):
+ super(OpensshRSACertificateInfo, self).__init__(**kwargs)
+ self.type_string = _SSH_TYPE_STRINGS['rsa'] + _CERT_SUFFIX_V01
+ self.e = e
+ self.n = n
+
+ # See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
+ def public_key_fingerprint(self):
+ if any([self.e is None, self.n is None]):
+ return b''
+
+ writer = _OpensshWriter()
+ writer.string(_SSH_TYPE_STRINGS['rsa'])
+ writer.mpint(self.e)
+ writer.mpint(self.n)
+
+ return fingerprint(writer.bytes())
+
+ def parse_public_numbers(self, parser):
+ self.e = parser.mpint()
+ self.n = parser.mpint()
+
+
+class OpensshDSACertificateInfo(OpensshCertificateInfo):
+ def __init__(self, p=None, q=None, g=None, y=None, **kwargs):
+ super(OpensshDSACertificateInfo, self).__init__(**kwargs)
+ self.type_string = _SSH_TYPE_STRINGS['dsa'] + _CERT_SUFFIX_V01
+ self.p = p
+ self.q = q
+ self.g = g
+ self.y = y
+
+ # See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
+ def public_key_fingerprint(self):
+ if any([self.p is None, self.q is None, self.g is None, self.y is None]):
+ return b''
+
+ writer = _OpensshWriter()
+ writer.string(_SSH_TYPE_STRINGS['dsa'])
+ writer.mpint(self.p)
+ writer.mpint(self.q)
+ writer.mpint(self.g)
+ writer.mpint(self.y)
+
+ return fingerprint(writer.bytes())
+
+ def parse_public_numbers(self, parser):
+ self.p = parser.mpint()
+ self.q = parser.mpint()
+ self.g = parser.mpint()
+ self.y = parser.mpint()
+
+
+class OpensshECDSACertificateInfo(OpensshCertificateInfo):
+ def __init__(self, curve=None, public_key=None, **kwargs):
+ super(OpensshECDSACertificateInfo, self).__init__(**kwargs)
+ self._curve = None
+ if curve is not None:
+ self.curve = curve
+
+ self.public_key = public_key
+
+ @property
+ def curve(self):
+ return self._curve
+
+ @curve.setter
+ def curve(self, curve):
+ if curve in _ECDSA_CURVE_IDENTIFIERS.values():
+ self._curve = curve
+ self.type_string = _SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[curve]] + _CERT_SUFFIX_V01
+ else:
+ raise ValueError(
+ "Curve must be one of %s" % (b','.join(list(_ECDSA_CURVE_IDENTIFIERS.values()))).decode('UTF-8')
+ )
+
+ # See https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
+ def public_key_fingerprint(self):
+ if any([self.curve is None, self.public_key is None]):
+ return b''
+
+ writer = _OpensshWriter()
+ writer.string(_SSH_TYPE_STRINGS[_ECDSA_CURVE_IDENTIFIERS_LOOKUP[self.curve]])
+ writer.string(self.curve)
+ writer.string(self.public_key)
+
+ return fingerprint(writer.bytes())
+
+ def parse_public_numbers(self, parser):
+ self.curve = parser.string()
+ self.public_key = parser.string()
+
+
+class OpensshED25519CertificateInfo(OpensshCertificateInfo):
+ def __init__(self, pk=None, **kwargs):
+ super(OpensshED25519CertificateInfo, self).__init__(**kwargs)
+ self.type_string = _SSH_TYPE_STRINGS['ed25519'] + _CERT_SUFFIX_V01
+ self.pk = pk
+
+ def public_key_fingerprint(self):
+ if self.pk is None:
+ return b''
+
+ writer = _OpensshWriter()
+ writer.string(_SSH_TYPE_STRINGS['ed25519'])
+ writer.string(self.pk)
+
+ return fingerprint(writer.bytes())
+
+ def parse_public_numbers(self, parser):
+ self.pk = parser.string()
+
+
+# See https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD
+class OpensshCertificate(object):
+ """Encapsulates a formatted OpenSSH certificate including signature and signing key"""
+ def __init__(self, cert_info, signature):
+
+ self._cert_info = cert_info
+ self.signature = signature
+
+ @classmethod
+ def load(cls, path):
+ if not os.path.exists(path):
+ raise ValueError("%s is not a valid path." % path)
+
+ try:
+ with open(path, 'rb') as cert_file:
+ data = cert_file.read()
+ except (IOError, OSError) as e:
+ raise ValueError("%s cannot be opened for reading: %s" % (path, e))
+
+ try:
+ format_identifier, b64_cert = data.split(b' ')[:2]
+ cert = binascii.a2b_base64(b64_cert)
+ except (binascii.Error, ValueError):
+ raise ValueError("Certificate not in OpenSSH format")
+
+ for key_type, string in _SSH_TYPE_STRINGS.items():
+ if format_identifier == string + _CERT_SUFFIX_V01:
+ pub_key_type = key_type
+ break
+ else:
+ raise ValueError("Invalid certificate format identifier: %s" % format_identifier)
+
+ parser = OpensshParser(cert)
+
+ if format_identifier != parser.string():
+ raise ValueError("Certificate formats do not match")
+
+ try:
+ cert_info = cls._parse_cert_info(pub_key_type, parser)
+ signature = parser.string()
+ except (TypeError, ValueError) as e:
+ raise ValueError("Invalid certificate data: %s" % e)
+
+ if parser.remaining_bytes():
+ raise ValueError(
+ "%s bytes of additional data was not parsed while loading %s" % (parser.remaining_bytes(), path)
+ )
+
+ return cls(
+ cert_info=cert_info,
+ signature=signature,
+ )
+
+ @property
+ def type_string(self):
+ return to_text(self._cert_info.type_string)
+
+ @property
+ def nonce(self):
+ return self._cert_info.nonce
+
+ @property
+ def public_key(self):
+ return to_text(self._cert_info.public_key_fingerprint())
+
+ @property
+ def serial(self):
+ return self._cert_info.serial
+
+ @property
+ def type(self):
+ return self._cert_info.cert_type
+
+ @property
+ def key_id(self):
+ return to_text(self._cert_info.key_id)
+
+ @property
+ def principals(self):
+ return [to_text(p) for p in self._cert_info.principals]
+
+ @property
+ def valid_after(self):
+ return self._cert_info.valid_after
+
+ @property
+ def valid_before(self):
+ return self._cert_info.valid_before
+
+ @property
+ def critical_options(self):
+ return [
+ OpensshCertificateOption('critical', to_text(n), to_text(d)) for n, d in self._cert_info.critical_options
+ ]
+
+ @property
+ def extensions(self):
+ return [OpensshCertificateOption('extension', to_text(n), to_text(d)) for n, d in self._cert_info.extensions]
+
+ @property
+ def reserved(self):
+ return self._cert_info.reserved
+
+ @property
+ def signing_key(self):
+ return to_text(self._cert_info.signing_key_fingerprint())
+
+ @property
+ def signature_type(self):
+ signature_data = OpensshParser.signature_data(self.signature)
+ return to_text(signature_data['signature_type'])
+
+ @staticmethod
+ def _parse_cert_info(pub_key_type, parser):
+ cert_info = get_cert_info_object(pub_key_type)
+ cert_info.nonce = parser.string()
+ cert_info.parse_public_numbers(parser)
+ cert_info.serial = parser.uint64()
+ cert_info.cert_type = parser.uint32()
+ cert_info.key_id = parser.string()
+ cert_info.principals = parser.string_list()
+ cert_info.valid_after = parser.uint64()
+ cert_info.valid_before = parser.uint64()
+ cert_info.critical_options = parser.option_list()
+ cert_info.extensions = parser.option_list()
+ cert_info.reserved = parser.string()
+ cert_info.signing_key = parser.string()
+
+ return cert_info
+
+ def to_dict(self):
+ time_parameters = OpensshCertificateTimeParameters(
+ valid_from=self.valid_after,
+ valid_to=self.valid_before
+ )
+ return {
+ 'type_string': self.type_string,
+ 'nonce': self.nonce,
+ 'serial': self.serial,
+ 'cert_type': self.type,
+ 'identifier': self.key_id,
+ 'principals': self.principals,
+ 'valid_after': time_parameters.valid_from(date_format='human_readable'),
+ 'valid_before': time_parameters.valid_to(date_format='human_readable'),
+ 'critical_options': [str(critical_option) for critical_option in self.critical_options],
+ 'extensions': [str(extension) for extension in self.extensions],
+ 'reserved': self.reserved,
+ 'public_key': self.public_key,
+ 'signing_key': self.signing_key,
+ }
+
+
+def apply_directives(directives):
+ if any(d not in _DIRECTIVES for d in directives):
+ raise ValueError("directives must be one of %s" % ", ".join(_DIRECTIVES))
+
+ directive_to_option = {
+ 'no-x11-forwarding': OpensshCertificateOption('extension', 'permit-x11-forwarding', ''),
+ 'no-agent-forwarding': OpensshCertificateOption('extension', 'permit-agent-forwarding', ''),
+ 'no-port-forwarding': OpensshCertificateOption('extension', 'permit-port-forwarding', ''),
+ 'no-pty': OpensshCertificateOption('extension', 'permit-pty', ''),
+ 'no-user-rc': OpensshCertificateOption('extension', 'permit-user-rc', ''),
+ }
+
+ if 'clear' in directives:
+ return []
+ else:
+ return list(set(default_options()) - set(directive_to_option[d] for d in directives))
+
+
+def default_options():
+ return [OpensshCertificateOption('extension', name, '') for name in _EXTENSIONS]
+
+
+def fingerprint(public_key):
+ """Generates a SHA256 hash and formats output to resemble ``ssh-keygen``"""
+ h = sha256()
+ h.update(public_key)
+ return b'SHA256:' + b64encode(h.digest()).rstrip(b'=')
+
+
+def get_cert_info_object(key_type):
+ if key_type == 'rsa':
+ cert_info = OpensshRSACertificateInfo()
+ elif key_type == 'dsa':
+ cert_info = OpensshDSACertificateInfo()
+ elif key_type in ('ecdsa-nistp256', 'ecdsa-nistp384', 'ecdsa-nistp521'):
+ cert_info = OpensshECDSACertificateInfo()
+ elif key_type == 'ed25519':
+ cert_info = OpensshED25519CertificateInfo()
+ else:
+ raise ValueError("%s is not a valid key type" % key_type)
+
+ return cert_info
+
+
+def get_option_type(name):
+ if name in _CRITICAL_OPTIONS:
+ result = 'critical'
+ elif name in _EXTENSIONS:
+ result = 'extension'
+ else:
+ raise ValueError("%s is not a valid option. " % name +
+ "Custom options must start with 'critical:' or 'extension:' to indicate type")
+ return result
+
+
+def is_relative_time_string(time_string):
+ return time_string.startswith("+") or time_string.startswith("-")
+
+
+def parse_option_list(option_list):
+ critical_options = []
+ directives = []
+ extensions = []
+
+ for option in option_list:
+ if option.lower() in _DIRECTIVES:
+ directives.append(option.lower())
+ else:
+ option_object = OpensshCertificateOption.from_string(option)
+ if option_object.type == 'critical':
+ critical_options.append(option_object)
+ else:
+ extensions.append(option_object)
+
+ return critical_options, list(set(extensions + apply_directives(directives)))
diff --git a/ansible_collections/community/crypto/plugins/module_utils/openssh/cryptography.py b/ansible_collections/community/crypto/plugins/module_utils/openssh/cryptography.py
new file mode 100644
index 00000000..69f3ce35
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/openssh/cryptography.py
@@ -0,0 +1,685 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+from base64 import b64encode, b64decode
+from getpass import getuser
+from socket import gethostname
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+try:
+ from cryptography import __version__ as CRYPTOGRAPHY_VERSION
+ from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
+ from cryptography.hazmat.backends.openssl import backend
+ from cryptography.hazmat.primitives import hashes, serialization
+ from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, padding
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
+
+ if LooseVersion(CRYPTOGRAPHY_VERSION) >= LooseVersion("3.0"):
+ HAS_OPENSSH_PRIVATE_FORMAT = True
+ else:
+ HAS_OPENSSH_PRIVATE_FORMAT = False
+
+ HAS_OPENSSH_SUPPORT = True
+
+ _ALGORITHM_PARAMETERS = {
+ 'rsa': {
+ 'default_size': 2048,
+ 'valid_sizes': range(1024, 16384),
+ 'signer_params': {
+ 'padding': padding.PSS(
+ mgf=padding.MGF1(hashes.SHA256()),
+ salt_length=padding.PSS.MAX_LENGTH,
+ ),
+ 'algorithm': hashes.SHA256(),
+ },
+ },
+ 'dsa': {
+ 'default_size': 1024,
+ 'valid_sizes': [1024],
+ 'signer_params': {
+ 'algorithm': hashes.SHA256(),
+ },
+ },
+ 'ed25519': {
+ 'default_size': 256,
+ 'valid_sizes': [256],
+ 'signer_params': {},
+ },
+ 'ecdsa': {
+ 'default_size': 256,
+ 'valid_sizes': [256, 384, 521],
+ 'signer_params': {
+ 'signature_algorithm': ec.ECDSA(hashes.SHA256()),
+ },
+ 'curves': {
+ 256: ec.SECP256R1(),
+ 384: ec.SECP384R1(),
+ 521: ec.SECP521R1(),
+ }
+ }
+ }
+except ImportError:
+ HAS_OPENSSH_PRIVATE_FORMAT = False
+ HAS_OPENSSH_SUPPORT = False
+ CRYPTOGRAPHY_VERSION = "0.0"
+ _ALGORITHM_PARAMETERS = {}
+
+_TEXT_ENCODING = 'UTF-8'
+
+
+class OpenSSHError(Exception):
+ pass
+
+
+class InvalidAlgorithmError(OpenSSHError):
+ pass
+
+
+class InvalidCommentError(OpenSSHError):
+ pass
+
+
+class InvalidDataError(OpenSSHError):
+ pass
+
+
+class InvalidPrivateKeyFileError(OpenSSHError):
+ pass
+
+
+class InvalidPublicKeyFileError(OpenSSHError):
+ pass
+
+
+class InvalidKeyFormatError(OpenSSHError):
+ pass
+
+
+class InvalidKeySizeError(OpenSSHError):
+ pass
+
+
+class InvalidKeyTypeError(OpenSSHError):
+ pass
+
+
+class InvalidPassphraseError(OpenSSHError):
+ pass
+
+
+class InvalidSignatureError(OpenSSHError):
+ pass
+
+
+class AsymmetricKeypair(object):
+ """Container for newly generated asymmetric key pairs or those loaded from existing files"""
+
+ @classmethod
+ def generate(cls, keytype='rsa', size=None, passphrase=None):
+ """Returns an Asymmetric_Keypair object generated with the supplied parameters
+ or defaults to an unencrypted RSA-2048 key
+
+ :keytype: One of rsa, dsa, ecdsa, ed25519
+ :size: The key length for newly generated keys
+ :passphrase: Secret of type Bytes used to encrypt the private key being generated
+ """
+
+ if keytype not in _ALGORITHM_PARAMETERS.keys():
+ raise InvalidKeyTypeError(
+ "%s is not a valid keytype. Valid keytypes are %s" % (
+ keytype, ", ".join(_ALGORITHM_PARAMETERS.keys())
+ )
+ )
+
+ if not size:
+ size = _ALGORITHM_PARAMETERS[keytype]['default_size']
+ else:
+ if size not in _ALGORITHM_PARAMETERS[keytype]['valid_sizes']:
+ raise InvalidKeySizeError(
+ "%s is not a valid key size for %s keys" % (size, keytype)
+ )
+
+ if passphrase:
+ encryption_algorithm = get_encryption_algorithm(passphrase)
+ else:
+ encryption_algorithm = serialization.NoEncryption()
+
+ if keytype == 'rsa':
+ privatekey = rsa.generate_private_key(
+ # Public exponent should always be 65537 to prevent issues
+ # if improper padding is used during signing
+ public_exponent=65537,
+ key_size=size,
+ backend=backend,
+ )
+ elif keytype == 'dsa':
+ privatekey = dsa.generate_private_key(
+ key_size=size,
+ backend=backend,
+ )
+ elif keytype == 'ed25519':
+ privatekey = Ed25519PrivateKey.generate()
+ elif keytype == 'ecdsa':
+ privatekey = ec.generate_private_key(
+ _ALGORITHM_PARAMETERS['ecdsa']['curves'][size],
+ backend=backend,
+ )
+
+ publickey = privatekey.public_key()
+
+ return cls(
+ keytype=keytype,
+ size=size,
+ privatekey=privatekey,
+ publickey=publickey,
+ encryption_algorithm=encryption_algorithm
+ )
+
+ @classmethod
+ def load(cls, path, passphrase=None, private_key_format='PEM', public_key_format='PEM', no_public_key=False):
+ """Returns an Asymmetric_Keypair object loaded from the supplied file path
+
+ :path: A path to an existing private key to be loaded
+ :passphrase: Secret of type bytes used to decrypt the private key being loaded
+ :private_key_format: Format of private key to be loaded
+ :public_key_format: Format of public key to be loaded
+ :no_public_key: Set 'True' to only load a private key and automatically populate the matching public key
+ """
+
+ if passphrase:
+ encryption_algorithm = get_encryption_algorithm(passphrase)
+ else:
+ encryption_algorithm = serialization.NoEncryption()
+
+ privatekey = load_privatekey(path, passphrase, private_key_format)
+ if no_public_key:
+ publickey = privatekey.public_key()
+ else:
+ publickey = load_publickey(path + '.pub', public_key_format)
+
+ # Ed25519 keys are always of size 256 and do not have a key_size attribute
+ if isinstance(privatekey, Ed25519PrivateKey):
+ size = _ALGORITHM_PARAMETERS['ed25519']['default_size']
+ else:
+ size = privatekey.key_size
+
+ if isinstance(privatekey, rsa.RSAPrivateKey):
+ keytype = 'rsa'
+ elif isinstance(privatekey, dsa.DSAPrivateKey):
+ keytype = 'dsa'
+ elif isinstance(privatekey, ec.EllipticCurvePrivateKey):
+ keytype = 'ecdsa'
+ elif isinstance(privatekey, Ed25519PrivateKey):
+ keytype = 'ed25519'
+ else:
+ raise InvalidKeyTypeError("Key type '%s' is not supported" % type(privatekey))
+
+ return cls(
+ keytype=keytype,
+ size=size,
+ privatekey=privatekey,
+ publickey=publickey,
+ encryption_algorithm=encryption_algorithm
+ )
+
+ def __init__(self, keytype, size, privatekey, publickey, encryption_algorithm):
+ """
+ :keytype: One of rsa, dsa, ecdsa, ed25519
+ :size: The key length for the private key of this key pair
+ :privatekey: Private key object of this key pair
+ :publickey: Public key object of this key pair
+ :encryption_algorithm: Hashed secret used to encrypt the private key of this key pair
+ """
+
+ self.__size = size
+ self.__keytype = keytype
+ self.__privatekey = privatekey
+ self.__publickey = publickey
+ self.__encryption_algorithm = encryption_algorithm
+
+ try:
+ self.verify(self.sign(b'message'), b'message')
+ except InvalidSignatureError:
+ raise InvalidPublicKeyFileError(
+ "The private key and public key of this keypair do not match"
+ )
+
+ def __eq__(self, other):
+ if not isinstance(other, AsymmetricKeypair):
+ return NotImplemented
+
+ return (compare_publickeys(self.public_key, other.public_key) and
+ compare_encryption_algorithms(self.encryption_algorithm, other.encryption_algorithm))
+
+ def __ne__(self, other):
+ return not self == other
+
+ @property
+ def private_key(self):
+ """Returns the private key of this key pair"""
+
+ return self.__privatekey
+
+ @property
+ def public_key(self):
+ """Returns the public key of this key pair"""
+
+ return self.__publickey
+
+ @property
+ def size(self):
+ """Returns the size of the private key of this key pair"""
+
+ return self.__size
+
+ @property
+ def key_type(self):
+ """Returns the key type of this key pair"""
+
+ return self.__keytype
+
+ @property
+ def encryption_algorithm(self):
+ """Returns the key encryption algorithm of this key pair"""
+
+ return self.__encryption_algorithm
+
+ def sign(self, data):
+ """Returns signature of data signed with the private key of this key pair
+
+ :data: byteslike data to sign
+ """
+
+ try:
+ signature = self.__privatekey.sign(
+ data,
+ **_ALGORITHM_PARAMETERS[self.__keytype]['signer_params']
+ )
+ except TypeError as e:
+ raise InvalidDataError(e)
+
+ return signature
+
+ def verify(self, signature, data):
+ """Verifies that the signature associated with the provided data was signed
+ by the private key of this key pair.
+
+ :signature: signature to verify
+ :data: byteslike data signed by the provided signature
+ """
+ try:
+ return self.__publickey.verify(
+ signature,
+ data,
+ **_ALGORITHM_PARAMETERS[self.__keytype]['signer_params']
+ )
+ except InvalidSignature:
+ raise InvalidSignatureError
+
+ def update_passphrase(self, passphrase=None):
+ """Updates the encryption algorithm of this key pair
+
+ :passphrase: Byte secret used to encrypt this key pair
+ """
+
+ if passphrase:
+ self.__encryption_algorithm = get_encryption_algorithm(passphrase)
+ else:
+ self.__encryption_algorithm = serialization.NoEncryption()
+
+
+class OpensshKeypair(object):
+ """Container for OpenSSH encoded asymmetric key pairs"""
+
+ @classmethod
+ def generate(cls, keytype='rsa', size=None, passphrase=None, comment=None):
+ """Returns an Openssh_Keypair object generated using the supplied parameters or defaults to a RSA-2048 key
+
+ :keytype: One of rsa, dsa, ecdsa, ed25519
+ :size: The key length for newly generated keys
+ :passphrase: Secret of type Bytes used to encrypt the newly generated private key
+ :comment: Comment for a newly generated OpenSSH public key
+ """
+
+ if comment is None:
+ comment = "%s@%s" % (getuser(), gethostname())
+
+ asym_keypair = AsymmetricKeypair.generate(keytype, size, passphrase)
+ openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH')
+ openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment)
+ fingerprint = calculate_fingerprint(openssh_publickey)
+
+ return cls(
+ asym_keypair=asym_keypair,
+ openssh_privatekey=openssh_privatekey,
+ openssh_publickey=openssh_publickey,
+ fingerprint=fingerprint,
+ comment=comment,
+ )
+
+ @classmethod
+ def load(cls, path, passphrase=None, no_public_key=False):
+ """Returns an Openssh_Keypair object loaded from the supplied file path
+
+ :path: A path to an existing private key to be loaded
+ :passphrase: Secret used to decrypt the private key being loaded
+ :no_public_key: Set 'True' to only load a private key and automatically populate the matching public key
+ """
+
+ if no_public_key:
+ comment = ""
+ else:
+ comment = extract_comment(path + '.pub')
+
+ asym_keypair = AsymmetricKeypair.load(path, passphrase, 'SSH', 'SSH', no_public_key)
+ openssh_privatekey = cls.encode_openssh_privatekey(asym_keypair, 'SSH')
+ openssh_publickey = cls.encode_openssh_publickey(asym_keypair, comment)
+ fingerprint = calculate_fingerprint(openssh_publickey)
+
+ return cls(
+ asym_keypair=asym_keypair,
+ openssh_privatekey=openssh_privatekey,
+ openssh_publickey=openssh_publickey,
+ fingerprint=fingerprint,
+ comment=comment,
+ )
+
+ @staticmethod
+ def encode_openssh_privatekey(asym_keypair, key_format):
+ """Returns an OpenSSH encoded private key for a given keypair
+
+ :asym_keypair: Asymmetric_Keypair from the private key is extracted
+ :key_format: Format of the encoded private key.
+ """
+
+ if key_format == 'SSH':
+ # Default to PEM format if SSH not available
+ if not HAS_OPENSSH_PRIVATE_FORMAT:
+ privatekey_format = serialization.PrivateFormat.PKCS8
+ else:
+ privatekey_format = serialization.PrivateFormat.OpenSSH
+ elif key_format == 'PKCS8':
+ privatekey_format = serialization.PrivateFormat.PKCS8
+ elif key_format == 'PKCS1':
+ if asym_keypair.key_type == 'ed25519':
+ raise InvalidKeyFormatError("ed25519 keys cannot be represented in PKCS1 format")
+ privatekey_format = serialization.PrivateFormat.TraditionalOpenSSL
+ else:
+ raise InvalidKeyFormatError("The accepted private key formats are SSH, PKCS8, and PKCS1")
+
+ encoded_privatekey = asym_keypair.private_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=privatekey_format,
+ encryption_algorithm=asym_keypair.encryption_algorithm
+ )
+
+ return encoded_privatekey
+
+ @staticmethod
+ def encode_openssh_publickey(asym_keypair, comment):
+ """Returns an OpenSSH encoded public key for a given keypair
+
+ :asym_keypair: Asymmetric_Keypair from the public key is extracted
+ :comment: Comment to apply to the end of the returned OpenSSH encoded public key
+ """
+ encoded_publickey = asym_keypair.public_key.public_bytes(
+ encoding=serialization.Encoding.OpenSSH,
+ format=serialization.PublicFormat.OpenSSH,
+ )
+
+ validate_comment(comment)
+
+ encoded_publickey += (" %s" % comment).encode(encoding=_TEXT_ENCODING) if comment else b''
+
+ return encoded_publickey
+
+ def __init__(self, asym_keypair, openssh_privatekey, openssh_publickey, fingerprint, comment):
+ """
+ :asym_keypair: An Asymmetric_Keypair object from which the OpenSSH encoded keypair is derived
+ :openssh_privatekey: An OpenSSH encoded private key
+ :openssh_privatekey: An OpenSSH encoded public key
+ :fingerprint: The fingerprint of the OpenSSH encoded public key of this keypair
+ :comment: Comment applied to the OpenSSH public key of this keypair
+ """
+
+ self.__asym_keypair = asym_keypair
+ self.__openssh_privatekey = openssh_privatekey
+ self.__openssh_publickey = openssh_publickey
+ self.__fingerprint = fingerprint
+ self.__comment = comment
+
+ def __eq__(self, other):
+ if not isinstance(other, OpensshKeypair):
+ return NotImplemented
+
+ return self.asymmetric_keypair == other.asymmetric_keypair and self.comment == other.comment
+
+ @property
+ def asymmetric_keypair(self):
+ """Returns the underlying asymmetric key pair of this OpenSSH encoded key pair"""
+
+ return self.__asym_keypair
+
+ @property
+ def private_key(self):
+ """Returns the OpenSSH formatted private key of this key pair"""
+
+ return self.__openssh_privatekey
+
+ @property
+ def public_key(self):
+ """Returns the OpenSSH formatted public key of this key pair"""
+
+ return self.__openssh_publickey
+
+ @property
+ def size(self):
+ """Returns the size of the private key of this key pair"""
+
+ return self.__asym_keypair.size
+
+ @property
+ def key_type(self):
+ """Returns the key type of this key pair"""
+
+ return self.__asym_keypair.key_type
+
+ @property
+ def fingerprint(self):
+ """Returns the fingerprint (SHA256 Hash) of the public key of this key pair"""
+
+ return self.__fingerprint
+
+ @property
+ def comment(self):
+ """Returns the comment applied to the OpenSSH formatted public key of this key pair"""
+
+ return self.__comment
+
+ @comment.setter
+ def comment(self, comment):
+ """Updates the comment applied to the OpenSSH formatted public key of this key pair
+
+ :comment: Text to update the OpenSSH public key comment
+ """
+
+ validate_comment(comment)
+
+ self.__comment = comment
+ encoded_comment = (" %s" % self.__comment).encode(encoding=_TEXT_ENCODING) if self.__comment else b''
+ self.__openssh_publickey = b' '.join(self.__openssh_publickey.split(b' ', 2)[:2]) + encoded_comment
+ return self.__openssh_publickey
+
+ def update_passphrase(self, passphrase):
+ """Updates the passphrase used to encrypt the private key of this keypair
+
+ :passphrase: Text secret used for encryption
+ """
+
+ self.__asym_keypair.update_passphrase(passphrase)
+ self.__openssh_privatekey = OpensshKeypair.encode_openssh_privatekey(self.__asym_keypair, 'SSH')
+
+
+def load_privatekey(path, passphrase, key_format):
+ privatekey_loaders = {
+ 'PEM': serialization.load_pem_private_key,
+ 'DER': serialization.load_der_private_key,
+ }
+
+ # OpenSSH formatted private keys are not available in Cryptography <3.0
+ if hasattr(serialization, 'load_ssh_private_key'):
+ privatekey_loaders['SSH'] = serialization.load_ssh_private_key
+ else:
+ privatekey_loaders['SSH'] = serialization.load_pem_private_key
+
+ try:
+ privatekey_loader = privatekey_loaders[key_format]
+ except KeyError:
+ raise InvalidKeyFormatError(
+ "%s is not a valid key format (%s)" % (
+ key_format,
+ ','.join(privatekey_loaders.keys())
+ )
+ )
+
+ if not os.path.exists(path):
+ raise InvalidPrivateKeyFileError("No file was found at %s" % path)
+
+ try:
+ with open(path, 'rb') as f:
+ content = f.read()
+
+ privatekey = privatekey_loader(
+ data=content,
+ password=passphrase,
+ backend=backend,
+ )
+
+ except ValueError as e:
+ # Revert to PEM if key could not be loaded in SSH format
+ if key_format == 'SSH':
+ try:
+ privatekey = privatekey_loaders['PEM'](
+ data=content,
+ password=passphrase,
+ backend=backend,
+ )
+ except ValueError as e:
+ raise InvalidPrivateKeyFileError(e)
+ except TypeError as e:
+ raise InvalidPassphraseError(e)
+ except UnsupportedAlgorithm as e:
+ raise InvalidAlgorithmError(e)
+ else:
+ raise InvalidPrivateKeyFileError(e)
+ except TypeError as e:
+ raise InvalidPassphraseError(e)
+ except UnsupportedAlgorithm as e:
+ raise InvalidAlgorithmError(e)
+
+ return privatekey
+
+
+def load_publickey(path, key_format):
+ publickey_loaders = {
+ 'PEM': serialization.load_pem_public_key,
+ 'DER': serialization.load_der_public_key,
+ 'SSH': serialization.load_ssh_public_key,
+ }
+
+ try:
+ publickey_loader = publickey_loaders[key_format]
+ except KeyError:
+ raise InvalidKeyFormatError(
+ "%s is not a valid key format (%s)" % (
+ key_format,
+ ','.join(publickey_loaders.keys())
+ )
+ )
+
+ if not os.path.exists(path):
+ raise InvalidPublicKeyFileError("No file was found at %s" % path)
+
+ try:
+ with open(path, 'rb') as f:
+ content = f.read()
+
+ publickey = publickey_loader(
+ data=content,
+ backend=backend,
+ )
+ except ValueError as e:
+ raise InvalidPublicKeyFileError(e)
+ except UnsupportedAlgorithm as e:
+ raise InvalidAlgorithmError(e)
+
+ return publickey
+
+
+def compare_publickeys(pk1, pk2):
+ a = isinstance(pk1, Ed25519PublicKey)
+ b = isinstance(pk2, Ed25519PublicKey)
+ if a or b:
+ if not a or not b:
+ return False
+ a = pk1.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
+ b = pk2.public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
+ return a == b
+ else:
+ return pk1.public_numbers() == pk2.public_numbers()
+
+
+def compare_encryption_algorithms(ea1, ea2):
+ if isinstance(ea1, serialization.NoEncryption) and isinstance(ea2, serialization.NoEncryption):
+ return True
+ elif (isinstance(ea1, serialization.BestAvailableEncryption) and
+ isinstance(ea2, serialization.BestAvailableEncryption)):
+ return ea1.password == ea2.password
+ else:
+ return False
+
+
+def get_encryption_algorithm(passphrase):
+ try:
+ return serialization.BestAvailableEncryption(passphrase)
+ except ValueError as e:
+ raise InvalidPassphraseError(e)
+
+
+def validate_comment(comment):
+ if not hasattr(comment, 'encode'):
+ raise InvalidCommentError("%s cannot be encoded to text" % comment)
+
+
+def extract_comment(path):
+
+ if not os.path.exists(path):
+ raise InvalidPublicKeyFileError("No file was found at %s" % path)
+
+ try:
+ with open(path, 'rb') as f:
+ fields = f.read().split(b' ', 2)
+ if len(fields) == 3:
+ comment = fields[2].decode(_TEXT_ENCODING)
+ else:
+ comment = ""
+ except (IOError, OSError) as e:
+ raise InvalidPublicKeyFileError(e)
+
+ return comment
+
+
+def calculate_fingerprint(openssh_publickey):
+ digest = hashes.Hash(hashes.SHA256(), backend=backend)
+ decoded_pubkey = b64decode(openssh_publickey.split(b' ')[1])
+ digest.update(decoded_pubkey)
+
+ return 'SHA256:%s' % b64encode(digest.finalize()).decode(encoding=_TEXT_ENCODING).rstrip('=')
diff --git a/ansible_collections/community/crypto/plugins/module_utils/openssh/utils.py b/ansible_collections/community/crypto/plugins/module_utils/openssh/utils.py
new file mode 100644
index 00000000..0c3af8f2
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/openssh/utils.py
@@ -0,0 +1,392 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2020, Doug Stanley <doug+ansible@technologixllc.com>
+# Copyright (c) 2021, Andrew Pantuso (@ajpantuso) <ajpantuso@gmail.com>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+import re
+from contextlib import contextmanager
+from struct import Struct
+
+from ansible.module_utils.six import PY3
+
+# Protocol References
+# -------------------
+# https://datatracker.ietf.org/doc/html/rfc4251
+# https://datatracker.ietf.org/doc/html/rfc4253
+# https://datatracker.ietf.org/doc/html/rfc5656
+# https://datatracker.ietf.org/doc/html/rfc8032
+#
+# Inspired by:
+# ------------
+# https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/serialization/ssh.py
+# https://github.com/paramiko/paramiko/blob/master/paramiko/message.py
+
+if PY3:
+ long = int
+
+# 0 (False) or 1 (True) encoded as a single byte
+_BOOLEAN = Struct(b'?')
+# Unsigned 8-bit integer in network-byte-order
+_UBYTE = Struct(b'!B')
+_UBYTE_MAX = 0xFF
+# Unsigned 32-bit integer in network-byte-order
+_UINT32 = Struct(b'!I')
+# Unsigned 32-bit little endian integer
+_UINT32_LE = Struct(b'<I')
+_UINT32_MAX = 0xFFFFFFFF
+# Unsigned 64-bit integer in network-byte-order
+_UINT64 = Struct(b'!Q')
+_UINT64_MAX = 0xFFFFFFFFFFFFFFFF
+
+
+def any_in(sequence, *elements):
+ return any(e in sequence for e in elements)
+
+
+def file_mode(path):
+ if not os.path.exists(path):
+ return 0o000
+ return os.stat(path).st_mode & 0o777
+
+
+def parse_openssh_version(version_string):
+ """Parse the version output of ssh -V and return version numbers that can be compared"""
+
+ parsed_result = re.match(
+ r"^.*openssh_(?P<version>[0-9.]+)(p?[0-9]+)[^0-9]*.*$", version_string.lower()
+ )
+ if parsed_result is not None:
+ version = parsed_result.group("version").strip()
+ else:
+ version = None
+
+ return version
+
+
+@contextmanager
+def secure_open(path, mode):
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
+ try:
+ yield fd
+ finally:
+ os.close(fd)
+
+
+def secure_write(path, mode, content):
+ with secure_open(path, mode) as fd:
+ os.write(fd, content)
+
+
+# See https://datatracker.ietf.org/doc/html/rfc4251#section-5 for SSH data types
+class OpensshParser(object):
+ """Parser for OpenSSH encoded objects"""
+ BOOLEAN_OFFSET = 1
+ UINT32_OFFSET = 4
+ UINT64_OFFSET = 8
+
+ def __init__(self, data):
+ if not isinstance(data, (bytes, bytearray)):
+ raise TypeError("Data must be bytes-like not %s" % type(data))
+
+ self._data = memoryview(data) if PY3 else data
+ self._pos = 0
+
+ def boolean(self):
+ next_pos = self._check_position(self.BOOLEAN_OFFSET)
+
+ value = _BOOLEAN.unpack(self._data[self._pos:next_pos])[0]
+ self._pos = next_pos
+ return value
+
+ def uint32(self):
+ next_pos = self._check_position(self.UINT32_OFFSET)
+
+ value = _UINT32.unpack(self._data[self._pos:next_pos])[0]
+ self._pos = next_pos
+ return value
+
+ def uint64(self):
+ next_pos = self._check_position(self.UINT64_OFFSET)
+
+ value = _UINT64.unpack(self._data[self._pos:next_pos])[0]
+ self._pos = next_pos
+ return value
+
+ def string(self):
+ length = self.uint32()
+
+ next_pos = self._check_position(length)
+
+ value = self._data[self._pos:next_pos]
+ self._pos = next_pos
+ # Cast to bytes is required as a memoryview slice is itself a memoryview
+ return value if not PY3 else bytes(value)
+
+ def mpint(self):
+ return self._big_int(self.string(), "big", signed=True)
+
+ def name_list(self):
+ raw_string = self.string()
+ return raw_string.decode('ASCII').split(',')
+
+ # Convenience function, but not an official data type from SSH
+ def string_list(self):
+ result = []
+ raw_string = self.string()
+
+ if raw_string:
+ parser = OpensshParser(raw_string)
+ while parser.remaining_bytes():
+ result.append(parser.string())
+
+ return result
+
+ # Convenience function, but not an official data type from SSH
+ def option_list(self):
+ result = []
+ raw_string = self.string()
+
+ if raw_string:
+ parser = OpensshParser(raw_string)
+
+ while parser.remaining_bytes():
+ name = parser.string()
+ data = parser.string()
+ if data:
+ # data is doubly-encoded
+ data = OpensshParser(data).string()
+ result.append((name, data))
+
+ return result
+
+ def seek(self, offset):
+ self._pos = self._check_position(offset)
+
+ return self._pos
+
+ def remaining_bytes(self):
+ return len(self._data) - self._pos
+
+ def _check_position(self, offset):
+ if self._pos + offset > len(self._data):
+ raise ValueError("Insufficient data remaining at position: %s" % self._pos)
+ elif self._pos + offset < 0:
+ raise ValueError("Position cannot be less than zero.")
+ else:
+ return self._pos + offset
+
+ @classmethod
+ def signature_data(cls, signature_string):
+ signature_data = {}
+
+ parser = cls(signature_string)
+ signature_type = parser.string()
+ signature_blob = parser.string()
+
+ blob_parser = cls(signature_blob)
+ if signature_type in (b'ssh-rsa', b'rsa-sha2-256', b'rsa-sha2-512'):
+ # https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
+ # https://datatracker.ietf.org/doc/html/rfc8332#section-3
+ signature_data['s'] = cls._big_int(signature_blob, "big")
+ elif signature_type == b'ssh-dss':
+ # https://datatracker.ietf.org/doc/html/rfc4253#section-6.6
+ signature_data['r'] = cls._big_int(signature_blob[:20], "big")
+ signature_data['s'] = cls._big_int(signature_blob[20:], "big")
+ elif signature_type in (b'ecdsa-sha2-nistp256', b'ecdsa-sha2-nistp384', b'ecdsa-sha2-nistp521'):
+ # https://datatracker.ietf.org/doc/html/rfc5656#section-3.1.2
+ signature_data['r'] = blob_parser.mpint()
+ signature_data['s'] = blob_parser.mpint()
+ elif signature_type == b'ssh-ed25519':
+ # https://datatracker.ietf.org/doc/html/rfc8032#section-5.1.2
+ signature_data['R'] = cls._big_int(signature_blob[:32], "little")
+ signature_data['S'] = cls._big_int(signature_blob[32:], "little")
+ else:
+ raise ValueError("%s is not a valid signature type" % signature_type)
+
+ signature_data['signature_type'] = signature_type
+
+ return signature_data
+
+ @classmethod
+ def _big_int(cls, raw_string, byte_order, signed=False):
+ if byte_order not in ("big", "little"):
+ raise ValueError("Byte_order must be one of (big, little) not %s" % byte_order)
+
+ if PY3:
+ return int.from_bytes(raw_string, byte_order, signed=signed)
+
+ result = 0
+ byte_length = len(raw_string)
+
+ if byte_length > 0:
+ # Check sign-bit
+ msb = raw_string[0] if byte_order == "big" else raw_string[-1]
+ negative = bool(ord(msb) & 0x80)
+ # Match pad value for two's complement
+ pad = b'\xFF' if signed and negative else b'\x00'
+ # The definition of ``mpint`` enforces that unnecessary bytes are not encoded so they are added back
+ pad_length = (4 - byte_length % 4)
+ if pad_length < 4:
+ raw_string = pad * pad_length + raw_string if byte_order == "big" else raw_string + pad * pad_length
+ byte_length += pad_length
+ # Accumulate arbitrary precision integer bytes in the appropriate order
+ if byte_order == "big":
+ for i in range(0, byte_length, cls.UINT32_OFFSET):
+ left_shift = result << cls.UINT32_OFFSET * 8
+ result = left_shift + _UINT32.unpack(raw_string[i:i + cls.UINT32_OFFSET])[0]
+ else:
+ for i in range(byte_length, 0, -cls.UINT32_OFFSET):
+ left_shift = result << cls.UINT32_OFFSET * 8
+ result = left_shift + _UINT32_LE.unpack(raw_string[i - cls.UINT32_OFFSET:i])[0]
+ # Adjust for two's complement
+ if signed and negative:
+ result -= 1 << (8 * byte_length)
+
+ return result
+
+
+class _OpensshWriter(object):
+ """Writes SSH encoded values to a bytes-like buffer
+
+ .. warning::
+ This class is a private API and must not be exported outside of the openssh module_utils.
+ It is not to be used to construct Openssh objects, but rather as a utility to assist
+ in validating parsed material.
+ """
+ def __init__(self, buffer=None):
+ if buffer is not None:
+ if not isinstance(buffer, (bytes, bytearray)):
+ raise TypeError("Buffer must be a bytes-like object not %s" % type(buffer))
+ else:
+ buffer = bytearray()
+
+ self._buff = buffer
+
+ def boolean(self, value):
+ if not isinstance(value, bool):
+ raise TypeError("Value must be of type bool not %s" % type(value))
+
+ self._buff.extend(_BOOLEAN.pack(value))
+
+ return self
+
+ def uint32(self, value):
+ if not isinstance(value, int):
+ raise TypeError("Value must be of type int not %s" % type(value))
+ if value < 0 or value > _UINT32_MAX:
+ raise ValueError("Value must be a positive integer less than %s" % _UINT32_MAX)
+
+ self._buff.extend(_UINT32.pack(value))
+
+ return self
+
+ def uint64(self, value):
+ if not isinstance(value, (long, int)):
+ raise TypeError("Value must be of type (long, int) not %s" % type(value))
+ if value < 0 or value > _UINT64_MAX:
+ raise ValueError("Value must be a positive integer less than %s" % _UINT64_MAX)
+
+ self._buff.extend(_UINT64.pack(value))
+
+ return self
+
+ def string(self, value):
+ if not isinstance(value, (bytes, bytearray)):
+ raise TypeError("Value must be bytes-like not %s" % type(value))
+ self.uint32(len(value))
+ self._buff.extend(value)
+
+ return self
+
+ def mpint(self, value):
+ if not isinstance(value, (int, long)):
+ raise TypeError("Value must be of type (long, int) not %s" % type(value))
+
+ self.string(self._int_to_mpint(value))
+
+ return self
+
+ def name_list(self, value):
+ if not isinstance(value, list):
+ raise TypeError("Value must be a list of byte strings not %s" % type(value))
+
+ try:
+ self.string(','.join(value).encode('ASCII'))
+ except UnicodeEncodeError as e:
+ raise ValueError("Name-list's must consist of US-ASCII characters: %s" % e)
+
+ return self
+
+ def string_list(self, value):
+ if not isinstance(value, list):
+ raise TypeError("Value must be a list of byte string not %s" % type(value))
+
+ writer = _OpensshWriter()
+ for s in value:
+ writer.string(s)
+
+ self.string(writer.bytes())
+
+ return self
+
+ def option_list(self, value):
+ if not isinstance(value, list) or (value and not isinstance(value[0], tuple)):
+ raise TypeError("Value must be a list of tuples")
+
+ writer = _OpensshWriter()
+ for name, data in value:
+ writer.string(name)
+ # SSH option data is encoded twice though this behavior is not documented
+ writer.string(_OpensshWriter().string(data).bytes() if data else bytes())
+
+ self.string(writer.bytes())
+
+ return self
+
+ @staticmethod
+ def _int_to_mpint(num):
+ if PY3:
+ byte_length = (num.bit_length() + 7) // 8
+ try:
+ result = num.to_bytes(byte_length, "big", signed=True)
+ # Handles values which require \x00 or \xFF to pad sign-bit
+ except OverflowError:
+ result = num.to_bytes(byte_length + 1, "big", signed=True)
+ else:
+ result = bytes()
+ # 0 and -1 are treated as special cases since they are used as sentinels for all other values
+ if num == 0:
+ result += b'\x00'
+ elif num == -1:
+ result += b'\xFF'
+ elif num > 0:
+ while num >> 32:
+ result = _UINT32.pack(num & _UINT32_MAX) + result
+ num = num >> 32
+ # Pack last 4 bytes individually to discard insignificant bytes
+ while num:
+ result = _UBYTE.pack(num & _UBYTE_MAX) + result
+ num = num >> 8
+ # Zero pad final byte if most-significant bit is 1 as per mpint definition
+ if ord(result[0]) & 0x80:
+ result = b'\x00' + result
+ else:
+ while (num >> 32) < -1:
+ result = _UINT32.pack(num & _UINT32_MAX) + result
+ num = num >> 32
+ while num < -1:
+ result = _UBYTE.pack(num & _UBYTE_MAX) + result
+ num = num >> 8
+ if not ord(result[0]) & 0x80:
+ result = b'\xFF' + result
+
+ return result
+
+ def bytes(self):
+ return bytes(self._buff)
diff --git a/ansible_collections/community/crypto/plugins/module_utils/version.py b/ansible_collections/community/crypto/plugins/module_utils/version.py
new file mode 100644
index 00000000..dc01ffe8
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/module_utils/version.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+"""Provide version object to compare version numbers."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+# Once we drop support for Ansible 2.9, ansible-base 2.10, and ansible-core 2.11, we can
+# remove the _version.py file, and replace the following import by
+#
+# from ansible.module_utils.compat.version import LooseVersion
+
+from ._version import LooseVersion # noqa: F401, pylint: disable=unused-import
diff --git a/ansible_collections/community/crypto/plugins/modules/acme_account.py b/ansible_collections/community/crypto/plugins/modules/acme_account.py
new file mode 100644
index 00000000..13de49ab
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/acme_account.py
@@ -0,0 +1,345 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: acme_account
+author: "Felix Fontein (@felixfontein)"
+short_description: Create, modify or delete ACME accounts
+description:
+ - "Allows to create, modify or delete accounts with a CA supporting the
+ L(ACME protocol,https://tools.ietf.org/html/rfc8555),
+ such as L(Let's Encrypt,https://letsencrypt.org/)."
+ - "This module only works with the ACME v2 protocol."
+notes:
+ - "The M(community.crypto.acme_certificate) module also allows to do basic account management.
+ When using both modules, it is recommended to disable account management
+ for M(community.crypto.acme_certificate). For that, use the C(modify_account) option of
+ M(community.crypto.acme_certificate)."
+seealso:
+ - name: Automatic Certificate Management Environment (ACME)
+ description: The specification of the ACME protocol (RFC 8555).
+ link: https://tools.ietf.org/html/rfc8555
+ - module: community.crypto.acme_account_info
+ description: Retrieves facts about an ACME account.
+ - module: community.crypto.openssl_privatekey
+ description: Can be used to create a private account key.
+ - module: community.crypto.openssl_privatekey_pipe
+ description: Can be used to create a private account key without writing it to disk.
+ - module: community.crypto.acme_inspect
+ description: Allows to debug problems.
+extends_documentation_fragment:
+ - community.crypto.acme
+ - community.crypto.attributes
+ - community.crypto.attributes.actiongroup_acme
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+options:
+ state:
+ description:
+ - "The state of the account, to be identified by its account key."
+ - "If the state is C(absent), the account will either not exist or be
+ deactivated."
+ - "If the state is C(changed_key), the account must exist. The account
+ key will be changed; no other information will be touched."
+ type: str
+ required: true
+ choices:
+ - present
+ - absent
+ - changed_key
+ allow_creation:
+ description:
+ - "Whether account creation is allowed (when state is C(present))."
+ type: bool
+ default: true
+ contact:
+ description:
+ - "A list of contact URLs."
+ - "Email addresses must be prefixed with C(mailto:)."
+ - "See U(https://tools.ietf.org/html/rfc8555#section-7.3)
+ for what is allowed."
+ - "Must be specified when state is C(present). Will be ignored
+ if state is C(absent) or C(changed_key)."
+ type: list
+ elements: str
+ default: []
+ terms_agreed:
+ description:
+ - "Boolean indicating whether you agree to the terms of service document."
+ - "ACME servers can require this to be true."
+ type: bool
+ default: false
+ new_account_key_src:
+ description:
+ - "Path to a file containing the ACME account RSA or Elliptic Curve key to change to."
+ - "Same restrictions apply as to C(account_key_src)."
+ - "Mutually exclusive with C(new_account_key_content)."
+ - "Required if C(new_account_key_content) is not used and state is C(changed_key)."
+ type: path
+ new_account_key_content:
+ description:
+ - "Content of the ACME account RSA or Elliptic Curve key to change to."
+ - "Same restrictions apply as to C(account_key_content)."
+ - "Mutually exclusive with C(new_account_key_src)."
+ - "Required if C(new_account_key_src) is not used and state is C(changed_key)."
+ type: str
+ new_account_key_passphrase:
+ description:
+ - Phassphrase to use to decode the new account key.
+ - "B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend."
+ type: str
+ version_added: 1.6.0
+ external_account_binding:
+ description:
+ - Allows to provide external account binding data during account creation.
+ - This is used by CAs like Sectigo to bind a new ACME account to an existing CA-specific
+ account, to be able to properly identify a customer.
+ - Only used when creating a new account. Can not be specified for ACME v1.
+ type: dict
+ suboptions:
+ kid:
+ description:
+ - The key identifier provided by the CA.
+ type: str
+ required: true
+ alg:
+ description:
+ - The MAC algorithm provided by the CA.
+ - If not specified by the CA, this is probably C(HS256).
+ type: str
+ required: true
+ choices: [ HS256, HS384, HS512 ]
+ key:
+ description:
+ - Base64 URL encoded value of the MAC key provided by the CA.
+ - Padding (C(=) symbols at the end) can be omitted.
+ type: str
+ required: true
+ version_added: 1.1.0
+'''
+
+EXAMPLES = '''
+- name: Make sure account exists and has given contacts. We agree to TOS.
+ community.crypto.acme_account:
+ account_key_src: /etc/pki/cert/private/account.key
+ state: present
+ terms_agreed: true
+ contact:
+ - mailto:me@example.com
+ - mailto:myself@example.org
+
+- name: Make sure account has given email address. Do not create account if it does not exist
+ community.crypto.acme_account:
+ account_key_src: /etc/pki/cert/private/account.key
+ state: present
+ allow_creation: false
+ contact:
+ - mailto:me@example.com
+
+- name: Change account's key to the one stored in the variable new_account_key
+ community.crypto.acme_account:
+ account_key_src: /etc/pki/cert/private/account.key
+ new_account_key_content: '{{ new_account_key }}'
+ state: changed_key
+
+- name: Delete account (we have to use the new key)
+ community.crypto.acme_account:
+ account_key_content: '{{ new_account_key }}'
+ state: absent
+'''
+
+RETURN = '''
+account_uri:
+ description: ACME account URI, or None if account does not exist.
+ returned: always
+ type: str
+'''
+
+import base64
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
+ create_backend,
+ get_default_argspec,
+ ACMEClient,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
+ ACMEAccount,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
+ ModuleFailException,
+ KeyParsingError,
+)
+
+
+def main():
+ argument_spec = get_default_argspec()
+ argument_spec.update(dict(
+ terms_agreed=dict(type='bool', default=False),
+ state=dict(type='str', required=True, choices=['absent', 'present', 'changed_key']),
+ allow_creation=dict(type='bool', default=True),
+ contact=dict(type='list', elements='str', default=[]),
+ new_account_key_src=dict(type='path'),
+ new_account_key_content=dict(type='str', no_log=True),
+ new_account_key_passphrase=dict(type='str', no_log=True),
+ external_account_binding=dict(type='dict', options=dict(
+ kid=dict(type='str', required=True),
+ alg=dict(type='str', required=True, choices=['HS256', 'HS384', 'HS512']),
+ key=dict(type='str', required=True, no_log=True),
+ ))
+ ))
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(
+ ['account_key_src', 'account_key_content'],
+ ),
+ mutually_exclusive=(
+ ['account_key_src', 'account_key_content'],
+ ['new_account_key_src', 'new_account_key_content'],
+ ),
+ required_if=(
+ # Make sure that for state == changed_key, one of
+ # new_account_key_src and new_account_key_content are specified
+ ['state', 'changed_key', ['new_account_key_src', 'new_account_key_content'], True],
+ ),
+ supports_check_mode=True,
+ )
+ backend = create_backend(module, True)
+
+ if module.params['external_account_binding']:
+ # Make sure padding is there
+ key = module.params['external_account_binding']['key']
+ if len(key) % 4 != 0:
+ key = key + ('=' * (4 - (len(key) % 4)))
+ # Make sure key is Base64 encoded
+ try:
+ base64.urlsafe_b64decode(key)
+ except Exception as e:
+ module.fail_json(msg='Key for external_account_binding must be Base64 URL encoded (%s)' % e)
+ module.params['external_account_binding']['key'] = key
+
+ try:
+ client = ACMEClient(module, backend)
+ account = ACMEAccount(client)
+ changed = False
+ state = module.params.get('state')
+ diff_before = {}
+ diff_after = {}
+ if state == 'absent':
+ created, account_data = account.setup_account(allow_creation=False)
+ if account_data:
+ diff_before = dict(account_data)
+ diff_before['public_account_key'] = client.account_key_data['jwk']
+ if created:
+ raise AssertionError('Unwanted account creation')
+ if account_data is not None:
+ # Account is not yet deactivated
+ if not module.check_mode:
+ # Deactivate it
+ payload = {
+ 'status': 'deactivated'
+ }
+ result, info = client.send_signed_request(
+ client.account_uri, payload, error_msg='Failed to deactivate account', expected_status_codes=[200])
+ changed = True
+ elif state == 'present':
+ allow_creation = module.params.get('allow_creation')
+ contact = [str(v) for v in module.params.get('contact')]
+ terms_agreed = module.params.get('terms_agreed')
+ external_account_binding = module.params.get('external_account_binding')
+ created, account_data = account.setup_account(
+ contact,
+ terms_agreed=terms_agreed,
+ allow_creation=allow_creation,
+ external_account_binding=external_account_binding,
+ )
+ if account_data is None:
+ raise ModuleFailException(msg='Account does not exist or is deactivated.')
+ if created:
+ diff_before = {}
+ else:
+ diff_before = dict(account_data)
+ diff_before['public_account_key'] = client.account_key_data['jwk']
+ updated = False
+ if not created:
+ updated, account_data = account.update_account(account_data, contact)
+ changed = created or updated
+ diff_after = dict(account_data)
+ diff_after['public_account_key'] = client.account_key_data['jwk']
+ elif state == 'changed_key':
+ # Parse new account key
+ try:
+ new_key_data = client.parse_key(
+ module.params.get('new_account_key_src'),
+ module.params.get('new_account_key_content'),
+ passphrase=module.params.get('new_account_key_passphrase'),
+ )
+ except KeyParsingError as e:
+ raise ModuleFailException("Error while parsing new account key: {msg}".format(msg=e.msg))
+ # Verify that the account exists and has not been deactivated
+ created, account_data = account.setup_account(allow_creation=False)
+ if created:
+ raise AssertionError('Unwanted account creation')
+ if account_data is None:
+ raise ModuleFailException(msg='Account does not exist or is deactivated.')
+ diff_before = dict(account_data)
+ diff_before['public_account_key'] = client.account_key_data['jwk']
+ # Now we can start the account key rollover
+ if not module.check_mode:
+ # Compose inner signed message
+ # https://tools.ietf.org/html/rfc8555#section-7.3.5
+ url = client.directory['keyChange']
+ protected = {
+ "alg": new_key_data['alg'],
+ "jwk": new_key_data['jwk'],
+ "url": url,
+ }
+ payload = {
+ "account": client.account_uri,
+ "newKey": new_key_data['jwk'], # specified in draft 12 and older
+ "oldKey": client.account_jwk, # specified in draft 13 and newer
+ }
+ data = client.sign_request(protected, payload, new_key_data)
+ # Send request and verify result
+ result, info = client.send_signed_request(
+ url, data, error_msg='Failed to rollover account key', expected_status_codes=[200])
+ if module._diff:
+ client.account_key_data = new_key_data
+ client.account_jws_header['alg'] = new_key_data['alg']
+ diff_after = account.get_account_data()
+ elif module._diff:
+ # Kind of fake diff_after
+ diff_after = dict(diff_before)
+ diff_after['public_account_key'] = new_key_data['jwk']
+ changed = True
+ result = {
+ 'changed': changed,
+ 'account_uri': client.account_uri,
+ }
+ if module._diff:
+ result['diff'] = {
+ 'before': diff_before,
+ 'after': diff_after,
+ }
+ module.exit_json(**result)
+ except ModuleFailException as e:
+ e.do_fail(module)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/acme_account_info.py b/ansible_collections/community/crypto/plugins/modules/acme_account_info.py
new file mode 100644
index 00000000..3f240649
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/acme_account_info.py
@@ -0,0 +1,320 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2018 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: acme_account_info
+author: "Felix Fontein (@felixfontein)"
+short_description: Retrieves information on ACME accounts
+description:
+ - "Allows to retrieve information on accounts a CA supporting the
+ L(ACME protocol,https://tools.ietf.org/html/rfc8555),
+ such as L(Let's Encrypt,https://letsencrypt.org/)."
+ - "This module only works with the ACME v2 protocol."
+notes:
+ - "The M(community.crypto.acme_account) module allows to modify, create and delete ACME
+ accounts."
+ - "This module was called C(acme_account_facts) before Ansible 2.8. The usage
+ did not change."
+extends_documentation_fragment:
+ - community.crypto.acme
+ - community.crypto.attributes
+ - community.crypto.attributes.actiongroup_acme
+ - community.crypto.attributes.info_module
+options:
+ retrieve_orders:
+ description:
+ - "Whether to retrieve the list of order URLs or order objects, if provided
+ by the ACME server."
+ - "A value of C(ignore) will not fetch the list of orders."
+ - "If the value is not C(ignore) and the ACME server supports orders, the C(order_uris)
+ return value is always populated. The C(orders) return value is only returned
+ if this option is set to C(object_list)."
+ - "Currently, Let's Encrypt does not return orders, so the C(orders) result
+ will always be empty."
+ type: str
+ choices:
+ - ignore
+ - url_list
+ - object_list
+ default: ignore
+seealso:
+ - module: community.crypto.acme_account
+ description: Allows to create, modify or delete an ACME account.
+
+'''
+
+EXAMPLES = '''
+- name: Check whether an account with the given account key exists
+ community.crypto.acme_account_info:
+ account_key_src: /etc/pki/cert/private/account.key
+ register: account_data
+- name: Verify that account exists
+ assert:
+ that:
+ - account_data.exists
+- name: Print account URI
+ ansible.builtin.debug:
+ var: account_data.account_uri
+- name: Print account contacts
+ ansible.builtin.debug:
+ var: account_data.account.contact
+
+- name: Check whether the account exists and is accessible with the given account key
+ acme_account_info:
+ account_key_content: "{{ acme_account_key }}"
+ account_uri: "{{ acme_account_uri }}"
+ register: account_data
+- name: Verify that account exists
+ assert:
+ that:
+ - account_data.exists
+- name: Print account contacts
+ ansible.builtin.debug:
+ var: account_data.account.contact
+'''
+
+RETURN = '''
+exists:
+ description: Whether the account exists.
+ returned: always
+ type: bool
+
+account_uri:
+ description: ACME account URI, or None if account does not exist.
+ returned: always
+ type: str
+
+account:
+ description: The account information, as retrieved from the ACME server.
+ returned: if account exists
+ type: dict
+ contains:
+ contact:
+ description: the challenge resource that must be created for validation
+ returned: always
+ type: list
+ elements: str
+ sample: ['mailto:me@example.com', 'tel:00123456789']
+ status:
+ description: the account's status
+ returned: always
+ type: str
+ choices: ['valid', 'deactivated', 'revoked']
+ sample: valid
+ orders:
+ description:
+ - A URL where a list of orders can be retrieved for this account.
+ - Use the I(retrieve_orders) option to query this URL and retrieve the
+ complete list of orders.
+ returned: always
+ type: str
+ sample: https://example.ca/account/1/orders
+ public_account_key:
+ description: the public account key as a L(JSON Web Key,https://tools.ietf.org/html/rfc7517).
+ returned: always
+ type: str
+ sample: '{"kty":"EC","crv":"P-256","x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4","y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM"}'
+
+orders:
+ description:
+ - "The list of orders."
+ type: list
+ elements: dict
+ returned: if account exists, I(retrieve_orders) is C(object_list), and server supports order listing
+ contains:
+ status:
+ description: The order's status.
+ type: str
+ choices:
+ - pending
+ - ready
+ - processing
+ - valid
+ - invalid
+ expires:
+ description:
+ - When the order expires.
+ - Timestamp should be formatted as described in RFC3339.
+ - Only required to be included in result when I(status) is C(pending) or C(valid).
+ type: str
+ returned: when server gives expiry date
+ identifiers:
+ description:
+ - List of identifiers this order is for.
+ type: list
+ elements: dict
+ contains:
+ type:
+ description: Type of identifier. C(dns) or C(ip).
+ type: str
+ value:
+ description: Name of identifier. Hostname or IP address.
+ type: str
+ wildcard:
+ description: "Whether I(value) is actually a wildcard. The wildcard
+ prefix C(*.) is not included in I(value) if this is C(true)."
+ type: bool
+ returned: required to be included if the identifier is wildcarded
+ notBefore:
+ description:
+ - The requested value of the C(notBefore) field in the certificate.
+ - Date should be formatted as described in RFC3339.
+ - Server is not required to return this.
+ type: str
+ returned: when server returns this
+ notAfter:
+ description:
+ - The requested value of the C(notAfter) field in the certificate.
+ - Date should be formatted as described in RFC3339.
+ - Server is not required to return this.
+ type: str
+ returned: when server returns this
+ error:
+ description:
+ - In case an error occurred during processing, this contains information about the error.
+ - The field is structured as a problem document (RFC7807).
+ type: dict
+ returned: when an error occurred
+ authorizations:
+ description:
+ - A list of URLs for authorizations for this order.
+ type: list
+ elements: str
+ finalize:
+ description:
+ - A URL used for finalizing an ACME order.
+ type: str
+ certificate:
+ description:
+ - The URL for retrieving the certificate.
+ type: str
+ returned: when certificate was issued
+
+order_uris:
+ description:
+ - "The list of orders."
+ - "If I(retrieve_orders) is C(url_list), this will be a list of URLs."
+ - "If I(retrieve_orders) is C(object_list), this will be a list of objects."
+ type: list
+ elements: str
+ returned: if account exists, I(retrieve_orders) is not C(ignore), and server supports order listing
+ version_added: 1.5.0
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
+ create_backend,
+ get_default_argspec,
+ ACMEClient,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
+ ACMEAccount,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
+ process_links,
+)
+
+
+def get_orders_list(module, client, orders_url):
+ '''
+ Retrieves orders list (handles pagination).
+ '''
+ orders = []
+ while orders_url:
+ # Get part of orders list
+ res, info = client.get_request(orders_url, parse_json_result=True, fail_on_error=True)
+ if not res.get('orders'):
+ if orders:
+ module.warn('When retrieving orders list part {0}, got empty result list'.format(orders_url))
+ break
+ # Add order URLs to result list
+ orders.extend(res['orders'])
+ # Extract URL of next part of results list
+ new_orders_url = []
+
+ def f(link, relation):
+ if relation == 'next':
+ new_orders_url.append(link)
+
+ process_links(info, f)
+ new_orders_url.append(None)
+ previous_orders_url, orders_url = orders_url, new_orders_url.pop(0)
+ if orders_url == previous_orders_url:
+ # Prevent infinite loop
+ orders_url = None
+ return orders
+
+
+def get_order(client, order_url):
+ '''
+ Retrieve order data.
+ '''
+ return client.get_request(order_url, parse_json_result=True, fail_on_error=True)[0]
+
+
+def main():
+ argument_spec = get_default_argspec()
+ argument_spec.update(dict(
+ retrieve_orders=dict(type='str', default='ignore', choices=['ignore', 'url_list', 'object_list']),
+ ))
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(
+ ['account_key_src', 'account_key_content'],
+ ),
+ mutually_exclusive=(
+ ['account_key_src', 'account_key_content'],
+ ),
+ supports_check_mode=True,
+ )
+ backend = create_backend(module, True)
+
+ try:
+ client = ACMEClient(module, backend)
+ account = ACMEAccount(client)
+ # Check whether account exists
+ created, account_data = account.setup_account(
+ [],
+ allow_creation=False,
+ remove_account_uri_if_not_exists=True,
+ )
+ if created:
+ raise AssertionError('Unwanted account creation')
+ result = {
+ 'changed': False,
+ 'exists': client.account_uri is not None,
+ 'account_uri': client.account_uri,
+ }
+ if client.account_uri is not None:
+ # Make sure promised data is there
+ if 'contact' not in account_data:
+ account_data['contact'] = []
+ account_data['public_account_key'] = client.account_key_data['jwk']
+ result['account'] = account_data
+ # Retrieve orders list
+ if account_data.get('orders') and module.params['retrieve_orders'] != 'ignore':
+ orders = get_orders_list(module, client, account_data['orders'])
+ result['order_uris'] = orders
+ if module.params['retrieve_orders'] == 'object_list':
+ result['orders'] = [get_order(client, order) for order in orders]
+ module.exit_json(**result)
+ except ModuleFailException as e:
+ e.do_fail(module)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/acme_certificate.py b/ansible_collections/community/crypto/plugins/modules/acme_certificate.py
new file mode 100644
index 00000000..274ed1d2
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/acme_certificate.py
@@ -0,0 +1,919 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: acme_certificate
+author: "Michael Gruener (@mgruener)"
+short_description: Create SSL/TLS certificates with the ACME protocol
+description:
+ - "Create and renew SSL/TLS certificates with a CA supporting the
+ L(ACME protocol,https://tools.ietf.org/html/rfc8555),
+ such as L(Let's Encrypt,https://letsencrypt.org/) or
+ L(Buypass,https://www.buypass.com/). The current implementation
+ supports the C(http-01), C(dns-01) and C(tls-alpn-01) challenges."
+ - "To use this module, it has to be executed twice. Either as two
+ different tasks in the same run or during two runs. Note that the output
+ of the first run needs to be recorded and passed to the second run as the
+ module argument C(data)."
+ - "Between these two tasks you have to fulfill the required steps for the
+ chosen challenge by whatever means necessary. For C(http-01) that means
+ creating the necessary challenge file on the destination webserver. For
+ C(dns-01) the necessary dns record has to be created. For C(tls-alpn-01)
+ the necessary certificate has to be created and served.
+ It is I(not) the responsibility of this module to perform these steps."
+ - "For details on how to fulfill these challenges, you might have to read through
+ L(the main ACME specification,https://tools.ietf.org/html/rfc8555#section-8)
+ and the L(TLS-ALPN-01 specification,https://www.rfc-editor.org/rfc/rfc8737.html#section-3).
+ Also, consider the examples provided for this module."
+ - "The module includes experimental support for IP identifiers according to
+ the L(RFC 8738,https://www.rfc-editor.org/rfc/rfc8738.html)."
+notes:
+ - "At least one of C(dest) and C(fullchain_dest) must be specified."
+ - "This module includes basic account management functionality.
+ If you want to have more control over your ACME account, use the
+ M(community.crypto.acme_account) module and disable account management
+ for this module using the C(modify_account) option."
+ - "This module was called C(letsencrypt) before Ansible 2.6. The usage
+ did not change."
+seealso:
+ - name: The Let's Encrypt documentation
+ description: Documentation for the Let's Encrypt Certification Authority.
+ Provides useful information for example on rate limits.
+ link: https://letsencrypt.org/docs/
+ - name: Buypass Go SSL
+ description: Documentation for the Buypass Certification Authority.
+ Provides useful information for example on rate limits.
+ link: https://www.buypass.com/ssl/products/acme
+ - name: Automatic Certificate Management Environment (ACME)
+ description: The specification of the ACME protocol (RFC 8555).
+ link: https://tools.ietf.org/html/rfc8555
+ - name: ACME TLS ALPN Challenge Extension
+ description: The specification of the C(tls-alpn-01) challenge (RFC 8737).
+ link: https://www.rfc-editor.org/rfc/rfc8737.html-05
+ - module: community.crypto.acme_challenge_cert_helper
+ description: Helps preparing C(tls-alpn-01) challenges.
+ - module: community.crypto.openssl_privatekey
+ description: Can be used to create private keys (both for certificates and accounts).
+ - module: community.crypto.openssl_privatekey_pipe
+ description: Can be used to create private keys without writing it to disk (both for certificates and accounts).
+ - module: community.crypto.openssl_csr
+ description: Can be used to create a Certificate Signing Request (CSR).
+ - module: community.crypto.openssl_csr_pipe
+ description: Can be used to create a Certificate Signing Request (CSR) without writing it to disk.
+ - module: community.crypto.certificate_complete_chain
+ description: Allows to find the root certificate for the returned fullchain.
+ - module: community.crypto.acme_certificate_revoke
+ description: Allows to revoke certificates.
+ - module: community.crypto.acme_account
+ description: Allows to create, modify or delete an ACME account.
+ - module: community.crypto.acme_inspect
+ description: Allows to debug problems.
+extends_documentation_fragment:
+ - community.crypto.acme
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+ - community.crypto.attributes.actiongroup_acme
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+ safe_file_operations:
+ support: full
+options:
+ account_email:
+ description:
+ - "The email address associated with this account."
+ - "It will be used for certificate expiration warnings."
+ - "Note that when C(modify_account) is not set to C(false) and you also
+ used the M(community.crypto.acme_account) module to specify more than one contact
+ for your account, this module will update your account and restrict
+ it to the (at most one) contact email address specified here."
+ type: str
+ agreement:
+ description:
+ - "URI to a terms of service document you agree to when using the
+ ACME v1 service at C(acme_directory)."
+ - Default is latest gathered from C(acme_directory) URL.
+ - This option will only be used when C(acme_version) is 1.
+ type: str
+ terms_agreed:
+ description:
+ - "Boolean indicating whether you agree to the terms of service document."
+ - "ACME servers can require this to be true."
+ - This option will only be used when C(acme_version) is not 1.
+ type: bool
+ default: false
+ modify_account:
+ description:
+ - "Boolean indicating whether the module should create the account if
+ necessary, and update its contact data."
+ - "Set to C(false) if you want to use the M(community.crypto.acme_account) module to manage
+ your account instead, and to avoid accidental creation of a new account
+ using an old key if you changed the account key with M(community.crypto.acme_account)."
+ - "If set to C(false), C(terms_agreed) and C(account_email) are ignored."
+ type: bool
+ default: true
+ challenge:
+ description: The challenge to be performed.
+ type: str
+ default: 'http-01'
+ choices: [ 'http-01', 'dns-01', 'tls-alpn-01' ]
+ csr:
+ description:
+ - "File containing the CSR for the new certificate."
+ - "Can be created with M(community.crypto.openssl_csr) or C(openssl req ...)."
+ - "The CSR may contain multiple Subject Alternate Names, but each one
+ will lead to an individual challenge that must be fulfilled for the
+ CSR to be signed."
+ - "I(Note): the private key used to create the CSR I(must not) be the
+ account key. This is a bad idea from a security point of view, and
+ the CA should not accept the CSR. The ACME server should return an
+ error in this case."
+ - Precisely one of I(csr) or I(csr_content) must be specified.
+ type: path
+ aliases: ['src']
+ csr_content:
+ description:
+ - "Content of the CSR for the new certificate."
+ - "Can be created with M(community.crypto.openssl_csr_pipe) or C(openssl req ...)."
+ - "The CSR may contain multiple Subject Alternate Names, but each one
+ will lead to an individual challenge that must be fulfilled for the
+ CSR to be signed."
+ - "I(Note): the private key used to create the CSR I(must not) be the
+ account key. This is a bad idea from a security point of view, and
+ the CA should not accept the CSR. The ACME server should return an
+ error in this case."
+ - Precisely one of I(csr) or I(csr_content) must be specified.
+ type: str
+ version_added: 1.2.0
+ data:
+ description:
+ - "The data to validate ongoing challenges. This must be specified for
+ the second run of the module only."
+ - "The value that must be used here will be provided by a previous use
+ of this module. See the examples for more details."
+ - "Note that for ACME v2, only the C(order_uri) entry of C(data) will
+ be used. For ACME v1, C(data) must be non-empty to indicate the
+ second stage is active; all needed data will be taken from the
+ CSR."
+ - "I(Note): the C(data) option was marked as C(no_log) up to
+ Ansible 2.5. From Ansible 2.6 on, it is no longer marked this way
+ as it causes error messages to be come unusable, and C(data) does
+ not contain any information which can be used without having
+ access to the account key or which are not public anyway."
+ type: dict
+ dest:
+ description:
+ - "The destination file for the certificate."
+ - "Required if C(fullchain_dest) is not specified."
+ type: path
+ aliases: ['cert']
+ fullchain_dest:
+ description:
+ - "The destination file for the full chain (that is, a certificate followed
+ by chain of intermediate certificates)."
+ - "Required if C(dest) is not specified."
+ type: path
+ aliases: ['fullchain']
+ chain_dest:
+ description:
+ - If specified, the intermediate certificate will be written to this file.
+ type: path
+ aliases: ['chain']
+ remaining_days:
+ description:
+ - "The number of days the certificate must have left being valid.
+ If C(cert_days < remaining_days), then it will be renewed.
+ If the certificate is not renewed, module return values will not
+ include C(challenge_data)."
+ - "To make sure that the certificate is renewed in any case, you can
+ use the C(force) option."
+ type: int
+ default: 10
+ deactivate_authzs:
+ description:
+ - "Deactivate authentication objects (authz) after issuing a certificate,
+ or when issuing the certificate failed."
+ - "Authentication objects are bound to an account key and remain valid
+ for a certain amount of time, and can be used to issue certificates
+ without having to re-authenticate the domain. This can be a security
+ concern."
+ type: bool
+ default: false
+ force:
+ description:
+ - Enforces the execution of the challenge and validation, even if an
+ existing certificate is still valid for more than C(remaining_days).
+ - This is especially helpful when having an updated CSR, for example with
+ additional domains for which a new certificate is desired.
+ type: bool
+ default: false
+ retrieve_all_alternates:
+ description:
+ - "When set to C(true), will retrieve all alternate trust chains offered by the ACME CA.
+ These will not be written to disk, but will be returned together with the main
+ chain as C(all_chains). See the documentation for the C(all_chains) return
+ value for details."
+ type: bool
+ default: false
+ select_chain:
+ description:
+ - "Allows to specify criteria by which an (alternate) trust chain can be selected."
+ - "The list of criteria will be processed one by one until a chain is found
+ matching a criterium. If such a chain is found, it will be used by the
+ module instead of the default chain."
+ - "If a criterium matches multiple chains, the first one matching will be
+ returned. The order is determined by the ordering of the C(Link) headers
+ returned by the ACME server and might not be deterministic."
+ - "Every criterium can consist of multiple different conditions, like I(issuer)
+ and I(subject). For the criterium to match a chain, all conditions must apply
+ to the same certificate in the chain."
+ - "This option can only be used with the C(cryptography) backend."
+ type: list
+ elements: dict
+ version_added: '1.0.0'
+ suboptions:
+ test_certificates:
+ description:
+ - "Determines which certificates in the chain will be tested."
+ - "I(all) tests all certificates in the chain (excluding the leaf, which is
+ identical in all chains)."
+ - "I(first) only tests the first certificate in the chain, that is the one which
+ signed the leaf."
+ - "I(last) only tests the last certificate in the chain, that is the one furthest
+ away from the leaf. Its issuer is the root certificate of this chain."
+ type: str
+ default: all
+ choices: [first, last, all]
+ issuer:
+ description:
+ - "Allows to specify parts of the issuer of a certificate in the chain must
+ have to be selected."
+ - "If I(issuer) is empty, any certificate will match."
+ - 'An example value would be C({"commonName": "My Preferred CA Root"}).'
+ type: dict
+ subject:
+ description:
+ - "Allows to specify parts of the subject of a certificate in the chain must
+ have to be selected."
+ - "If I(subject) is empty, any certificate will match."
+ - 'An example value would be C({"CN": "My Preferred CA Intermediate"})'
+ type: dict
+ subject_key_identifier:
+ description:
+ - "Checks for the SubjectKeyIdentifier extension. This is an identifier based
+ on the private key of the intermediate certificate."
+ - "The identifier must be of the form
+ C(A8:4A:6A:63:04:7D:DD:BA:E6:D1:39:B7:A6:45:65:EF:F3:A8:EC:A1)."
+ type: str
+ authority_key_identifier:
+ description:
+ - "Checks for the AuthorityKeyIdentifier extension. This is an identifier based
+ on the private key of the issuer of the intermediate certificate."
+ - "The identifier must be of the form
+ C(C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10)."
+ type: str
+'''
+
+EXAMPLES = r'''
+### Example with HTTP challenge ###
+
+- name: Create a challenge for sample.com using a account key from a variable.
+ community.crypto.acme_certificate:
+ account_key_content: "{{ account_private_key }}"
+ csr: /etc/pki/cert/csr/sample.com.csr
+ dest: /etc/httpd/ssl/sample.com.crt
+ register: sample_com_challenge
+
+# Alternative first step:
+- name: Create a challenge for sample.com using a account key from hashi vault.
+ community.crypto.acme_certificate:
+ account_key_content: "{{ lookup('hashi_vault', 'secret=secret/account_private_key:value') }}"
+ csr: /etc/pki/cert/csr/sample.com.csr
+ fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
+ register: sample_com_challenge
+
+# Alternative first step:
+- name: Create a challenge for sample.com using a account key file.
+ community.crypto.acme_certificate:
+ account_key_src: /etc/pki/cert/private/account.key
+ csr_content: "{{ lookup('file', '/etc/pki/cert/csr/sample.com.csr') }}"
+ dest: /etc/httpd/ssl/sample.com.crt
+ fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
+ register: sample_com_challenge
+
+# perform the necessary steps to fulfill the challenge
+# for example:
+#
+# - copy:
+# dest: /var/www/html/{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource'] }}
+# content: "{{ sample_com_challenge['challenge_data']['sample.com']['http-01']['resource_value'] }}"
+# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge['challenge_data']
+#
+# Alternative way:
+#
+# - copy:
+# dest: /var/www/{{ item.key }}/{{ item.value['http-01']['resource'] }}
+# content: "{{ item.value['http-01']['resource_value'] }}"
+# loop: "{{ sample_com_challenge.challenge_data | dict2items }}"
+# when: sample_com_challenge is changed
+
+- name: Let the challenge be validated and retrieve the cert and intermediate certificate
+ community.crypto.acme_certificate:
+ account_key_src: /etc/pki/cert/private/account.key
+ csr: /etc/pki/cert/csr/sample.com.csr
+ dest: /etc/httpd/ssl/sample.com.crt
+ fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
+ chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
+ data: "{{ sample_com_challenge }}"
+
+### Example with DNS challenge against production ACME server ###
+
+- name: Create a challenge for sample.com using a account key file.
+ community.crypto.acme_certificate:
+ account_key_src: /etc/pki/cert/private/account.key
+ account_email: myself@sample.com
+ src: /etc/pki/cert/csr/sample.com.csr
+ cert: /etc/httpd/ssl/sample.com.crt
+ challenge: dns-01
+ acme_directory: https://acme-v01.api.letsencrypt.org/directory
+ # Renew if the certificate is at least 30 days old
+ remaining_days: 60
+ register: sample_com_challenge
+
+# perform the necessary steps to fulfill the challenge
+# for example:
+#
+# - community.aws.route53:
+# zone: sample.com
+# record: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].record }}"
+# type: TXT
+# ttl: 60
+# state: present
+# wait: true
+# # Note: route53 requires TXT entries to be enclosed in quotes
+# value: "{{ sample_com_challenge.challenge_data['sample.com']['dns-01'].resource_value | regex_replace('^(.*)$', '\"\\1\"') }}"
+# when: sample_com_challenge is changed and 'sample.com' in sample_com_challenge.challenge_data
+#
+# Alternative way:
+#
+# - community.aws.route53:
+# zone: sample.com
+# record: "{{ item.key }}"
+# type: TXT
+# ttl: 60
+# state: present
+# wait: true
+# # Note: item.value is a list of TXT entries, and route53
+# # requires every entry to be enclosed in quotes
+# value: "{{ item.value | map('regex_replace', '^(.*)$', '\"\\1\"' ) | list }}"
+# loop: "{{ sample_com_challenge.challenge_data_dns | dict2items }}"
+# when: sample_com_challenge is changed
+
+- name: Let the challenge be validated and retrieve the cert and intermediate certificate
+ community.crypto.acme_certificate:
+ account_key_src: /etc/pki/cert/private/account.key
+ account_email: myself@sample.com
+ src: /etc/pki/cert/csr/sample.com.csr
+ cert: /etc/httpd/ssl/sample.com.crt
+ fullchain: /etc/httpd/ssl/sample.com-fullchain.crt
+ chain: /etc/httpd/ssl/sample.com-intermediate.crt
+ challenge: dns-01
+ acme_directory: https://acme-v01.api.letsencrypt.org/directory
+ remaining_days: 60
+ data: "{{ sample_com_challenge }}"
+ when: sample_com_challenge is changed
+
+# Alternative second step:
+- name: Let the challenge be validated and retrieve the cert and intermediate certificate
+ community.crypto.acme_certificate:
+ account_key_src: /etc/pki/cert/private/account.key
+ account_email: myself@sample.com
+ src: /etc/pki/cert/csr/sample.com.csr
+ cert: /etc/httpd/ssl/sample.com.crt
+ fullchain: /etc/httpd/ssl/sample.com-fullchain.crt
+ chain: /etc/httpd/ssl/sample.com-intermediate.crt
+ challenge: tls-alpn-01
+ remaining_days: 60
+ data: "{{ sample_com_challenge }}"
+ # We use Let's Encrypt's ACME v2 endpoint
+ acme_directory: https://acme-v02.api.letsencrypt.org/directory
+ acme_version: 2
+ # The following makes sure that if a chain with /CN=DST Root CA X3 in its issuer is provided
+ # as an alternative, it will be selected. These are the roots cross-signed by IdenTrust.
+ # As long as Let's Encrypt provides alternate chains with the cross-signed root(s) when
+ # switching to their own ISRG Root X1 root, this will use the chain ending with a cross-signed
+ # root. This chain is more compatible with older TLS clients.
+ select_chain:
+ - test_certificates: last
+ issuer:
+ CN: DST Root CA X3
+ O: Digital Signature Trust Co.
+ when: sample_com_challenge is changed
+'''
+
+RETURN = '''
+cert_days:
+ description: The number of days the certificate remains valid.
+ returned: success
+ type: int
+challenge_data:
+ description:
+ - Per identifier / challenge type challenge data.
+ - Since Ansible 2.8.5, only challenges which are not yet valid are returned.
+ returned: changed
+ type: list
+ elements: dict
+ contains:
+ resource:
+ description: The challenge resource that must be created for validation.
+ returned: changed
+ type: str
+ sample: .well-known/acme-challenge/evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA
+ resource_original:
+ description:
+ - The original challenge resource including type identifier for C(tls-alpn-01)
+ challenges.
+ returned: changed and challenge is C(tls-alpn-01)
+ type: str
+ sample: DNS:example.com
+ resource_value:
+ description:
+ - The value the resource has to produce for the validation.
+ - For C(http-01) and C(dns-01) challenges, the value can be used as-is.
+ - "For C(tls-alpn-01) challenges, note that this return value contains a
+ Base64 encoded version of the correct binary blob which has to be put
+ into the acmeValidation x509 extension; see
+ U(https://www.rfc-editor.org/rfc/rfc8737.html#section-3)
+ for details. To do this, you might need the C(b64decode) Jinja filter
+ to extract the binary blob from this return value."
+ returned: changed
+ type: str
+ sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
+ record:
+ description: The full DNS record's name for the challenge.
+ returned: changed and challenge is C(dns-01)
+ type: str
+ sample: _acme-challenge.example.com
+challenge_data_dns:
+ description:
+ - List of TXT values per DNS record, in case challenge is C(dns-01).
+ - Since Ansible 2.8.5, only challenges which are not yet valid are returned.
+ returned: changed
+ type: dict
+authorizations:
+ description:
+ - ACME authorization data.
+ - Maps an identifier to ACME authorization objects. See U(https://tools.ietf.org/html/rfc8555#section-7.1.4).
+ returned: changed
+ type: dict
+ sample:
+ example.com:
+ identifier:
+ type: dns
+ value: example.com
+ status: valid
+ expires: '2022-08-04T01:02:03.45Z'
+ challenges:
+ - url: https://example.org/acme/challenge/12345
+ type: http-01
+ status: valid
+ token: A5b1C3d2E9f8G7h6
+ validated: '2022-08-01T01:01:02.34Z'
+ wildcard: false
+order_uri:
+ description: ACME order URI.
+ returned: changed
+ type: str
+finalization_uri:
+ description: ACME finalization URI.
+ returned: changed
+ type: str
+account_uri:
+ description: ACME account URI.
+ returned: changed
+ type: str
+all_chains:
+ description:
+ - When I(retrieve_all_alternates) is set to C(true), the module will query the ACME server
+ for alternate chains. This return value will contain a list of all chains returned,
+ the first entry being the main chain returned by the server.
+ - See L(Section 7.4.2 of RFC8555,https://tools.ietf.org/html/rfc8555#section-7.4.2) for details.
+ returned: when certificate was retrieved and I(retrieve_all_alternates) is set to C(true)
+ type: list
+ elements: dict
+ contains:
+ cert:
+ description:
+ - The leaf certificate itself, in PEM format.
+ type: str
+ returned: always
+ chain:
+ description:
+ - The certificate chain, excluding the root, as concatenated PEM certificates.
+ type: str
+ returned: always
+ full_chain:
+ description:
+ - The certificate chain, excluding the root, but including the leaf certificate,
+ as concatenated PEM certificates.
+ type: str
+ returned: always
+'''
+
+import os
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
+ create_backend,
+ get_default_argspec,
+ ACMEClient,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
+ ACMEAccount,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
+ combine_identifier,
+ split_identifier,
+ Authorization,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import (
+ retrieve_acme_v1_certificate,
+ CertificateChain,
+ Criterium,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
+ ModuleFailException,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
+ write_file,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.orders import (
+ Order,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
+ pem_to_der,
+)
+
+
+class ACMECertificateClient(object):
+ '''
+ ACME client class. Uses an ACME account object and a CSR to
+ start and validate ACME challenges and download the respective
+ certificates.
+ '''
+
+ def __init__(self, module, backend):
+ self.module = module
+ self.version = module.params['acme_version']
+ self.challenge = module.params['challenge']
+ self.csr = module.params['csr']
+ self.csr_content = module.params['csr_content']
+ self.dest = module.params.get('dest')
+ self.fullchain_dest = module.params.get('fullchain_dest')
+ self.chain_dest = module.params.get('chain_dest')
+ self.client = ACMEClient(module, backend)
+ self.account = ACMEAccount(self.client)
+ self.directory = self.client.directory
+ self.data = module.params['data']
+ self.authorizations = None
+ self.cert_days = -1
+ self.order = None
+ self.order_uri = self.data.get('order_uri') if self.data else None
+ self.all_chains = None
+ self.select_chain_matcher = []
+
+ if self.module.params['select_chain']:
+ for criterium_idx, criterium in enumerate(self.module.params['select_chain']):
+ try:
+ self.select_chain_matcher.append(
+ self.client.backend.create_chain_matcher(
+ Criterium(criterium, index=criterium_idx)))
+ except ValueError as exc:
+ self.module.warn('Error while parsing criterium: {error}. Ignoring criterium.'.format(error=exc))
+
+ # Make sure account exists
+ modify_account = module.params['modify_account']
+ if modify_account or self.version > 1:
+ contact = []
+ if module.params['account_email']:
+ contact.append('mailto:' + module.params['account_email'])
+ created, account_data = self.account.setup_account(
+ contact,
+ agreement=module.params.get('agreement'),
+ terms_agreed=module.params.get('terms_agreed'),
+ allow_creation=modify_account,
+ )
+ if account_data is None:
+ raise ModuleFailException(msg='Account does not exist or is deactivated.')
+ updated = False
+ if not created and account_data and modify_account:
+ updated, account_data = self.account.update_account(account_data, contact)
+ self.changed = created or updated
+ else:
+ # This happens if modify_account is False and the ACME v1
+ # protocol is used. In this case, we do not call setup_account()
+ # to avoid accidental creation of an account. This is OK
+ # since for ACME v1, the account URI is not needed to send a
+ # signed ACME request.
+ pass
+
+ if self.csr is not None and not os.path.exists(self.csr):
+ raise ModuleFailException("CSR %s not found" % (self.csr))
+
+ # Extract list of identifiers from CSR
+ self.identifiers = self.client.backend.get_csr_identifiers(csr_filename=self.csr, csr_content=self.csr_content)
+
+ def is_first_step(self):
+ '''
+ Return True if this is the first execution of this module, i.e. if a
+ sufficient data object from a first run has not been provided.
+ '''
+ if self.data is None:
+ return True
+ if self.version == 1:
+ # As soon as self.data is a non-empty object, we are in the second stage.
+ return not self.data
+ else:
+ # We are in the second stage if data.order_uri is given (which has been
+ # stored in self.order_uri by the constructor).
+ return self.order_uri is None
+
+ def start_challenges(self):
+ '''
+ Create new authorizations for all identifiers of the CSR,
+ respectively start a new order for ACME v2.
+ '''
+ self.authorizations = {}
+ if self.version == 1:
+ for identifier_type, identifier in self.identifiers:
+ if identifier_type != 'dns':
+ raise ModuleFailException('ACME v1 only supports DNS identifiers!')
+ for identifier_type, identifier in self.identifiers:
+ authz = Authorization.create(self.client, identifier_type, identifier)
+ self.authorizations[authz.combined_identifier] = authz
+ else:
+ self.order = Order.create(self.client, self.identifiers)
+ self.order_uri = self.order.url
+ self.order.load_authorizations(self.client)
+ self.authorizations.update(self.order.authorizations)
+ self.changed = True
+
+ def get_challenges_data(self, first_step):
+ '''
+ Get challenge details for the chosen challenge type.
+ Return a tuple of generic challenge details, and specialized DNS challenge details.
+ '''
+ # Get general challenge data
+ data = {}
+ for type_identifier, authz in self.authorizations.items():
+ identifier_type, identifier = split_identifier(type_identifier)
+ # Skip valid authentications: their challenges are already valid
+ # and do not need to be returned
+ if authz.status == 'valid':
+ continue
+ # We drop the type from the key to preserve backwards compatibility
+ data[identifier] = authz.get_challenge_data(self.client)
+ if first_step and self.challenge not in data[identifier]:
+ raise ModuleFailException("Found no challenge of type '{0}' for identifier {1}!".format(
+ self.challenge, type_identifier))
+ # Get DNS challenge data
+ data_dns = {}
+ if self.challenge == 'dns-01':
+ for identifier, challenges in data.items():
+ if self.challenge in challenges:
+ values = data_dns.get(challenges[self.challenge]['record'])
+ if values is None:
+ values = []
+ data_dns[challenges[self.challenge]['record']] = values
+ values.append(challenges[self.challenge]['resource_value'])
+ return data, data_dns
+
+ def finish_challenges(self):
+ '''
+ Verify challenges for all identifiers of the CSR.
+ '''
+ self.authorizations = {}
+
+ # Step 1: obtain challenge information
+ if self.version == 1:
+ # For ACME v1, we attempt to create new authzs. Existing ones
+ # will be returned instead.
+ for identifier_type, identifier in self.identifiers:
+ authz = Authorization.create(self.client, identifier_type, identifier)
+ self.authorizations[combine_identifier(identifier_type, identifier)] = authz
+ else:
+ # For ACME v2, we obtain the order object by fetching the
+ # order URI, and extract the information from there.
+ self.order = Order.from_url(self.client, self.order_uri)
+ self.order.load_authorizations(self.client)
+ self.authorizations.update(self.order.authorizations)
+
+ # Step 2: validate pending challenges
+ for type_identifier, authz in self.authorizations.items():
+ if authz.status == 'pending':
+ identifier_type, identifier = split_identifier(type_identifier)
+ authz.call_validate(self.client, self.challenge)
+ self.changed = True
+
+ def download_alternate_chains(self, cert):
+ alternate_chains = []
+ for alternate in cert.alternates:
+ try:
+ alt_cert = CertificateChain.download(self.client, alternate)
+ except ModuleFailException as e:
+ self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e))
+ continue
+ alternate_chains.append(alt_cert)
+ return alternate_chains
+
+ def find_matching_chain(self, chains):
+ for criterium_idx, matcher in enumerate(self.select_chain_matcher):
+ for chain in chains:
+ if matcher.match(chain):
+ self.module.debug('Found matching chain for criterium {0}'.format(criterium_idx))
+ return chain
+ return None
+
+ def get_certificate(self):
+ '''
+ Request a new certificate and write it to the destination file.
+ First verifies whether all authorizations are valid; if not, aborts
+ with an error.
+ '''
+ for identifier_type, identifier in self.identifiers:
+ authz = self.authorizations.get(combine_identifier(identifier_type, identifier))
+ if authz is None:
+ raise ModuleFailException('Found no authorization information for "{identifier}"!'.format(
+ identifier=combine_identifier(identifier_type, identifier)))
+ if authz.status != 'valid':
+ authz.raise_error('Status is "{status}" and not "valid"'.format(status=authz.status), module=self.module)
+
+ if self.version == 1:
+ cert = retrieve_acme_v1_certificate(self.client, pem_to_der(self.csr, self.csr_content))
+ else:
+ self.order.finalize(self.client, pem_to_der(self.csr, self.csr_content))
+ cert = CertificateChain.download(self.client, self.order.certificate_uri)
+ if self.module.params['retrieve_all_alternates'] or self.select_chain_matcher:
+ # Retrieve alternate chains
+ alternate_chains = self.download_alternate_chains(cert)
+
+ # Prepare return value for all alternate chains
+ if self.module.params['retrieve_all_alternates']:
+ self.all_chains = [cert.to_json()]
+ for alt_chain in alternate_chains:
+ self.all_chains.append(alt_chain.to_json())
+
+ # Try to select alternate chain depending on criteria
+ if self.select_chain_matcher:
+ matching_chain = self.find_matching_chain([cert] + alternate_chains)
+ if matching_chain:
+ cert = matching_chain
+ else:
+ self.module.debug('Found no matching alternative chain')
+
+ if cert.cert is not None:
+ pem_cert = cert.cert
+ chain = cert.chain
+
+ if self.dest and write_file(self.module, self.dest, pem_cert.encode('utf8')):
+ self.cert_days = self.client.backend.get_cert_days(self.dest)
+ self.changed = True
+
+ if self.fullchain_dest and write_file(self.module, self.fullchain_dest, (pem_cert + "\n".join(chain)).encode('utf8')):
+ self.cert_days = self.client.backend.get_cert_days(self.fullchain_dest)
+ self.changed = True
+
+ if self.chain_dest and write_file(self.module, self.chain_dest, ("\n".join(chain)).encode('utf8')):
+ self.changed = True
+
+ def deactivate_authzs(self):
+ '''
+ Deactivates all valid authz's. Does not raise exceptions.
+ https://community.letsencrypt.org/t/authorization-deactivation/19860/2
+ https://tools.ietf.org/html/rfc8555#section-7.5.2
+ '''
+ for authz in self.authorizations.values():
+ try:
+ authz.deactivate(self.client)
+ except Exception:
+ # ignore errors
+ pass
+ if authz.status != 'deactivated':
+ self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz.url))
+
+
+def main():
+ argument_spec = get_default_argspec()
+ argument_spec.update(dict(
+ modify_account=dict(type='bool', default=True),
+ account_email=dict(type='str'),
+ agreement=dict(type='str'),
+ terms_agreed=dict(type='bool', default=False),
+ challenge=dict(type='str', default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01']),
+ csr=dict(type='path', aliases=['src']),
+ csr_content=dict(type='str'),
+ data=dict(type='dict'),
+ dest=dict(type='path', aliases=['cert']),
+ fullchain_dest=dict(type='path', aliases=['fullchain']),
+ chain_dest=dict(type='path', aliases=['chain']),
+ remaining_days=dict(type='int', default=10),
+ deactivate_authzs=dict(type='bool', default=False),
+ force=dict(type='bool', default=False),
+ retrieve_all_alternates=dict(type='bool', default=False),
+ select_chain=dict(type='list', elements='dict', options=dict(
+ test_certificates=dict(type='str', default='all', choices=['first', 'last', 'all']),
+ issuer=dict(type='dict'),
+ subject=dict(type='dict'),
+ subject_key_identifier=dict(type='str'),
+ authority_key_identifier=dict(type='str'),
+ )),
+ ))
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(
+ ['account_key_src', 'account_key_content'],
+ ['dest', 'fullchain_dest'],
+ ['csr', 'csr_content'],
+ ),
+ mutually_exclusive=(
+ ['account_key_src', 'account_key_content'],
+ ['csr', 'csr_content'],
+ ),
+ supports_check_mode=True,
+ )
+ backend = create_backend(module, False)
+
+ try:
+ if module.params.get('dest'):
+ cert_days = backend.get_cert_days(module.params['dest'])
+ else:
+ cert_days = backend.get_cert_days(module.params['fullchain_dest'])
+
+ if module.params['force'] or cert_days < module.params['remaining_days']:
+ # If checkmode is active, base the changed state solely on the status
+ # of the certificate file as all other actions (accessing an account, checking
+ # the authorization status...) would lead to potential changes of the current
+ # state
+ if module.check_mode:
+ module.exit_json(changed=True, authorizations={}, challenge_data={}, cert_days=cert_days)
+ else:
+ client = ACMECertificateClient(module, backend)
+ client.cert_days = cert_days
+ other = dict()
+ is_first_step = client.is_first_step()
+ if is_first_step:
+ # First run: start challenges / start new order
+ client.start_challenges()
+ else:
+ # Second run: finish challenges, and get certificate
+ try:
+ client.finish_challenges()
+ client.get_certificate()
+ if client.all_chains is not None:
+ other['all_chains'] = client.all_chains
+ finally:
+ if module.params['deactivate_authzs']:
+ client.deactivate_authzs()
+ data, data_dns = client.get_challenges_data(first_step=is_first_step)
+ auths = dict()
+ for k, v in client.authorizations.items():
+ # Remove "type:" from key
+ auths[split_identifier(k)[1]] = v.to_json()
+ module.exit_json(
+ changed=client.changed,
+ authorizations=auths,
+ finalize_uri=client.order.finalize_uri if client.order else None,
+ order_uri=client.order_uri,
+ account_uri=client.client.account_uri,
+ challenge_data=data,
+ challenge_data_dns=data_dns,
+ cert_days=client.cert_days,
+ **other
+ )
+ else:
+ module.exit_json(changed=False, cert_days=cert_days)
+ except ModuleFailException as e:
+ e.do_fail(module)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/acme_certificate_revoke.py b/ansible_collections/community/crypto/plugins/modules/acme_certificate_revoke.py
new file mode 100644
index 00000000..f1922384
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/acme_certificate_revoke.py
@@ -0,0 +1,245 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016 Michael Gruener <michael.gruener@chaosmoon.net>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: acme_certificate_revoke
+author: "Felix Fontein (@felixfontein)"
+short_description: Revoke certificates with the ACME protocol
+description:
+ - "Allows to revoke certificates issued by a CA supporting the
+ L(ACME protocol,https://tools.ietf.org/html/rfc8555),
+ such as L(Let's Encrypt,https://letsencrypt.org/)."
+notes:
+ - "Exactly one of C(account_key_src), C(account_key_content),
+ C(private_key_src) or C(private_key_content) must be specified."
+ - "Trying to revoke an already revoked certificate
+ should result in an unchanged status, even if the revocation reason
+ was different than the one specified here. Also, depending on the
+ server, it can happen that some other error is returned if the
+ certificate has already been revoked."
+seealso:
+ - name: The Let's Encrypt documentation
+ description: Documentation for the Let's Encrypt Certification Authority.
+ Provides useful information for example on rate limits.
+ link: https://letsencrypt.org/docs/
+ - name: Automatic Certificate Management Environment (ACME)
+ description: The specification of the ACME protocol (RFC 8555).
+ link: https://tools.ietf.org/html/rfc8555
+ - module: community.crypto.acme_inspect
+ description: Allows to debug problems.
+extends_documentation_fragment:
+ - community.crypto.acme
+ - community.crypto.attributes
+ - community.crypto.attributes.actiongroup_acme
+attributes:
+ check_mode:
+ support: none
+ diff_mode:
+ support: none
+options:
+ certificate:
+ description:
+ - "Path to the certificate to revoke."
+ type: path
+ required: true
+ account_key_src:
+ description:
+ - "Path to a file containing the ACME account RSA or Elliptic Curve
+ key."
+ - "RSA keys can be created with C(openssl rsa ...). Elliptic curve keys can
+ be created with C(openssl ecparam -genkey ...). Any other tool creating
+ private keys in PEM format can be used as well."
+ - "Mutually exclusive with C(account_key_content)."
+ - "Required if C(account_key_content) is not used."
+ account_key_content:
+ description:
+ - "Content of the ACME account RSA or Elliptic Curve key."
+ - "Note that exactly one of C(account_key_src), C(account_key_content),
+ C(private_key_src) or C(private_key_content) must be specified."
+ - "I(Warning): the content will be written into a temporary file, which will
+ be deleted by Ansible when the module completes. Since this is an
+ important private key — it can be used to change the account key,
+ or to revoke your certificates without knowing their private keys
+ —, this might not be acceptable."
+ - "In case C(cryptography) is used, the content is not written into a
+ temporary file. It can still happen that it is written to disk by
+ Ansible in the process of moving the module with its argument to
+ the node where it is executed."
+ private_key_src:
+ description:
+ - "Path to the certificate's private key."
+ - "Note that exactly one of C(account_key_src), C(account_key_content),
+ C(private_key_src) or C(private_key_content) must be specified."
+ type: path
+ private_key_content:
+ description:
+ - "Content of the certificate's private key."
+ - "Note that exactly one of C(account_key_src), C(account_key_content),
+ C(private_key_src) or C(private_key_content) must be specified."
+ - "I(Warning): the content will be written into a temporary file, which will
+ be deleted by Ansible when the module completes. Since this is an
+ important private key — it can be used to change the account key,
+ or to revoke your certificates without knowing their private keys
+ —, this might not be acceptable."
+ - "In case C(cryptography) is used, the content is not written into a
+ temporary file. It can still happen that it is written to disk by
+ Ansible in the process of moving the module with its argument to
+ the node where it is executed."
+ type: str
+ private_key_passphrase:
+ description:
+ - Phassphrase to use to decode the certificate's private key.
+ - "B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend."
+ type: str
+ version_added: 1.6.0
+ revoke_reason:
+ description:
+ - "One of the revocation reasonCodes defined in
+ L(Section 5.3.1 of RFC5280,https://tools.ietf.org/html/rfc5280#section-5.3.1)."
+ - "Possible values are C(0) (unspecified), C(1) (keyCompromise),
+ C(2) (cACompromise), C(3) (affiliationChanged), C(4) (superseded),
+ C(5) (cessationOfOperation), C(6) (certificateHold),
+ C(8) (removeFromCRL), C(9) (privilegeWithdrawn),
+ C(10) (aACompromise)."
+ type: int
+'''
+
+EXAMPLES = '''
+- name: Revoke certificate with account key
+ community.crypto.acme_certificate_revoke:
+ account_key_src: /etc/pki/cert/private/account.key
+ certificate: /etc/httpd/ssl/sample.com.crt
+
+- name: Revoke certificate with certificate's private key
+ community.crypto.acme_certificate_revoke:
+ private_key_src: /etc/httpd/ssl/sample.com.key
+ certificate: /etc/httpd/ssl/sample.com.crt
+'''
+
+RETURN = '''#'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
+ create_backend,
+ get_default_argspec,
+ ACMEClient,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.account import (
+ ACMEAccount,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
+ ACMEProtocolException,
+ ModuleFailException,
+ KeyParsingError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
+ nopad_b64,
+ pem_to_der,
+)
+
+
+def main():
+ argument_spec = get_default_argspec()
+ argument_spec.update(dict(
+ private_key_src=dict(type='path'),
+ private_key_content=dict(type='str', no_log=True),
+ private_key_passphrase=dict(type='str', no_log=True),
+ certificate=dict(type='path', required=True),
+ revoke_reason=dict(type='int'),
+ ))
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ required_one_of=(
+ ['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
+ ),
+ mutually_exclusive=(
+ ['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
+ ),
+ supports_check_mode=False,
+ )
+ backend = create_backend(module, False)
+
+ try:
+ client = ACMEClient(module, backend)
+ account = ACMEAccount(client)
+ # Load certificate
+ certificate = pem_to_der(module.params.get('certificate'))
+ certificate = nopad_b64(certificate)
+ # Construct payload
+ payload = {
+ 'certificate': certificate
+ }
+ if module.params.get('revoke_reason') is not None:
+ payload['reason'] = module.params.get('revoke_reason')
+ # Determine endpoint
+ if module.params.get('acme_version') == 1:
+ endpoint = client.directory['revoke-cert']
+ payload['resource'] = 'revoke-cert'
+ else:
+ endpoint = client.directory['revokeCert']
+ # Get hold of private key (if available) and make sure it comes from disk
+ private_key = module.params.get('private_key_src')
+ private_key_content = module.params.get('private_key_content')
+ # Revoke certificate
+ if private_key or private_key_content:
+ passphrase = module.params['private_key_passphrase']
+ # Step 1: load and parse private key
+ try:
+ private_key_data = client.parse_key(private_key, private_key_content, passphrase=passphrase)
+ except KeyParsingError as e:
+ raise ModuleFailException("Error while parsing private key: {msg}".format(msg=e.msg))
+ # Step 2: sign revokation request with private key
+ jws_header = {
+ "alg": private_key_data['alg'],
+ "jwk": private_key_data['jwk'],
+ }
+ result, info = client.send_signed_request(
+ endpoint, payload, key_data=private_key_data, jws_header=jws_header, fail_on_error=False)
+ else:
+ # Step 1: get hold of account URI
+ created, account_data = account.setup_account(allow_creation=False)
+ if created:
+ raise AssertionError('Unwanted account creation')
+ if account_data is None:
+ raise ModuleFailException(msg='Account does not exist or is deactivated.')
+ # Step 2: sign revokation request with account key
+ result, info = client.send_signed_request(endpoint, payload, fail_on_error=False)
+ if info['status'] != 200:
+ already_revoked = False
+ # Standardized error from draft 14 on (https://tools.ietf.org/html/rfc8555#section-7.6)
+ if result.get('type') == 'urn:ietf:params:acme:error:alreadyRevoked':
+ already_revoked = True
+ else:
+ # Hack for Boulder errors
+ if module.params.get('acme_version') == 1:
+ error_type = 'urn:acme:error:malformed'
+ else:
+ error_type = 'urn:ietf:params:acme:error:malformed'
+ if result.get('type') == error_type and result.get('detail') == 'Certificate already revoked':
+ # Fallback: boulder returns this in case the certificate was already revoked.
+ already_revoked = True
+ # If we know the certificate was already revoked, we do not fail,
+ # but successfully terminate while indicating no change
+ if already_revoked:
+ module.exit_json(changed=False)
+ raise ACMEProtocolException(module, 'Failed to revoke certificate', info=info, content_json=result)
+ module.exit_json(changed=True)
+ except ModuleFailException as e:
+ e.do_fail(module)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/acme_challenge_cert_helper.py b/ansible_collections/community/crypto/plugins/modules/acme_challenge_cert_helper.py
new file mode 100644
index 00000000..1b963e8c
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/acme_challenge_cert_helper.py
@@ -0,0 +1,319 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2018 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: acme_challenge_cert_helper
+author: "Felix Fontein (@felixfontein)"
+short_description: Prepare certificates required for ACME challenges such as C(tls-alpn-01)
+description:
+ - "Prepares certificates for ACME challenges such as C(tls-alpn-01)."
+ - "The raw data is provided by the M(community.crypto.acme_certificate) module, and needs to be
+ converted to a certificate to be used for challenge validation. This module
+ provides a simple way to generate the required certificates."
+seealso:
+ - name: Automatic Certificate Management Environment (ACME)
+ description: The specification of the ACME protocol (RFC 8555).
+ link: https://tools.ietf.org/html/rfc8555
+ - name: ACME TLS ALPN Challenge Extension
+ description: The specification of the C(tls-alpn-01) challenge (RFC 8737).
+ link: https://www.rfc-editor.org/rfc/rfc8737.html
+requirements:
+ - "cryptography >= 1.3"
+extends_documentation_fragment:
+ - community.crypto.attributes
+attributes:
+ check_mode:
+ support: none
+ details:
+ - This action does not modify state.
+ diff_mode:
+ support: N/A
+ details:
+ - This action does not modify state.
+options:
+ challenge:
+ description:
+ - "The challenge type."
+ type: str
+ required: true
+ choices:
+ - tls-alpn-01
+ challenge_data:
+ description:
+ - "The C(challenge_data) entry provided by M(community.crypto.acme_certificate) for the
+ challenge."
+ type: dict
+ required: true
+ private_key_src:
+ description:
+ - "Path to a file containing the private key file to use for this challenge
+ certificate."
+ - "Mutually exclusive with C(private_key_content)."
+ type: path
+ private_key_content:
+ description:
+ - "Content of the private key to use for this challenge certificate."
+ - "Mutually exclusive with C(private_key_src)."
+ type: str
+ private_key_passphrase:
+ description:
+ - Phassphrase to use to decode the private key.
+ type: str
+ version_added: 1.6.0
+'''
+
+EXAMPLES = '''
+- name: Create challenges for a given CRT for sample.com
+ community.crypto.acme_certificate:
+ account_key_src: /etc/pki/cert/private/account.key
+ challenge: tls-alpn-01
+ csr: /etc/pki/cert/csr/sample.com.csr
+ dest: /etc/httpd/ssl/sample.com.crt
+ register: sample_com_challenge
+
+- name: Create certificates for challenges
+ community.crypto.acme_challenge_cert_helper:
+ challenge: tls-alpn-01
+ challenge_data: "{{ item.value['tls-alpn-01'] }}"
+ private_key_src: /etc/pki/cert/key/sample.com.key
+ loop: "{{ sample_com_challenge.challenge_data | dictsort }}"
+ register: sample_com_challenge_certs
+
+- name: Install challenge certificates
+ # We need to set up HTTPS such that for the domain,
+ # regular_certificate is delivered for regular connections,
+ # except if ALPN selects the "acme-tls/1"; then, the
+ # challenge_certificate must be delivered.
+ # This can for example be achieved with very new versions
+ # of NGINX; search for ssl_preread and
+ # ssl_preread_alpn_protocols for information on how to
+ # route by ALPN protocol.
+ ...:
+ domain: "{{ item.domain }}"
+ challenge_certificate: "{{ item.challenge_certificate }}"
+ regular_certificate: "{{ item.regular_certificate }}"
+ private_key: /etc/pki/cert/key/sample.com.key
+ loop: "{{ sample_com_challenge_certs.results }}"
+
+- name: Create certificate for a given CSR for sample.com
+ community.crypto.acme_certificate:
+ account_key_src: /etc/pki/cert/private/account.key
+ challenge: tls-alpn-01
+ csr: /etc/pki/cert/csr/sample.com.csr
+ dest: /etc/httpd/ssl/sample.com.crt
+ data: "{{ sample_com_challenge }}"
+'''
+
+RETURN = '''
+domain:
+ description:
+ - "The domain the challenge is for. The certificate should be provided if
+ this is specified in the request's the C(Host) header."
+ returned: always
+ type: str
+identifier_type:
+ description:
+ - "The identifier type for the actual resource identifier. Will be C(dns)
+ or C(ip)."
+ returned: always
+ type: str
+identifier:
+ description:
+ - "The identifier for the actual resource. Will be a domain name if the
+ type is C(dns), or an IP address if the type is C(ip)."
+ returned: always
+ type: str
+challenge_certificate:
+ description:
+ - "The challenge certificate in PEM format."
+ returned: always
+ type: str
+regular_certificate:
+ description:
+ - "A self-signed certificate for the challenge domain."
+ - "If no existing certificate exists, can be used to set-up
+ https in the first place if that is needed for providing
+ the challenge."
+ returned: always
+ type: str
+'''
+
+import base64
+import datetime
+import sys
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.common.text.converters import to_bytes, to_text
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import ModuleFailException
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.io import (
+ read_file,
+)
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ import cryptography.hazmat.backends
+ import cryptography.hazmat.primitives.serialization
+ import cryptography.hazmat.primitives.asymmetric.rsa
+ import cryptography.hazmat.primitives.asymmetric.ec
+ import cryptography.hazmat.primitives.asymmetric.padding
+ import cryptography.hazmat.primitives.hashes
+ import cryptography.hazmat.primitives.asymmetric.utils
+ import cryptography.x509
+ import cryptography.x509.oid
+ import ipaddress
+ HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.3'))
+ _cryptography_backend = cryptography.hazmat.backends.default_backend()
+except ImportError as dummy:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ HAS_CRYPTOGRAPHY = False
+
+
+# Convert byte string to ASN1 encoded octet string
+if sys.version_info[0] >= 3:
+ def encode_octet_string(octet_string):
+ if len(octet_string) >= 128:
+ raise ModuleFailException('Cannot handle octet strings with more than 128 bytes')
+ return bytes([0x4, len(octet_string)]) + octet_string
+else:
+ def encode_octet_string(octet_string):
+ if len(octet_string) >= 128:
+ raise ModuleFailException('Cannot handle octet strings with more than 128 bytes')
+ return b'\x04' + chr(len(octet_string)) + octet_string
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ challenge=dict(type='str', required=True, choices=['tls-alpn-01']),
+ challenge_data=dict(type='dict', required=True),
+ private_key_src=dict(type='path'),
+ private_key_content=dict(type='str', no_log=True),
+ private_key_passphrase=dict(type='str', no_log=True),
+ ),
+ required_one_of=(
+ ['private_key_src', 'private_key_content'],
+ ),
+ mutually_exclusive=(
+ ['private_key_src', 'private_key_content'],
+ ),
+ )
+ if not HAS_CRYPTOGRAPHY:
+ # Some callbacks die when exception is provided with value None
+ if CRYPTOGRAPHY_IMP_ERR:
+ module.fail_json(msg=missing_required_lib('cryptography >= 1.3'), exception=CRYPTOGRAPHY_IMP_ERR)
+ module.fail_json(msg=missing_required_lib('cryptography >= 1.3'))
+
+ try:
+ # Get parameters
+ challenge = module.params['challenge']
+ challenge_data = module.params['challenge_data']
+
+ # Get hold of private key
+ private_key_content = module.params.get('private_key_content')
+ private_key_passphrase = module.params.get('private_key_passphrase')
+ if private_key_content is None:
+ private_key_content = read_file(module.params['private_key_src'])
+ else:
+ private_key_content = to_bytes(private_key_content)
+ try:
+ private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
+ private_key_content,
+ password=to_bytes(private_key_passphrase) if private_key_passphrase is not None else None,
+ backend=_cryptography_backend)
+ except Exception as e:
+ raise ModuleFailException('Error while loading private key: {0}'.format(e))
+
+ # Some common attributes
+ domain = to_text(challenge_data['resource'])
+ identifier_type, identifier = to_text(challenge_data.get('resource_original', 'dns:' + challenge_data['resource'])).split(':', 1)
+ subject = issuer = cryptography.x509.Name([])
+ not_valid_before = datetime.datetime.utcnow()
+ not_valid_after = datetime.datetime.utcnow() + datetime.timedelta(days=10)
+ if identifier_type == 'dns':
+ san = cryptography.x509.DNSName(identifier)
+ elif identifier_type == 'ip':
+ san = cryptography.x509.IPAddress(ipaddress.ip_address(identifier))
+ else:
+ raise ModuleFailException('Unsupported identifier type "{0}"'.format(identifier_type))
+
+ # Generate regular self-signed certificate
+ regular_certificate = cryptography.x509.CertificateBuilder().subject_name(
+ subject
+ ).issuer_name(
+ issuer
+ ).public_key(
+ private_key.public_key()
+ ).serial_number(
+ cryptography.x509.random_serial_number()
+ ).not_valid_before(
+ not_valid_before
+ ).not_valid_after(
+ not_valid_after
+ ).add_extension(
+ cryptography.x509.SubjectAlternativeName([san]),
+ critical=False,
+ ).sign(
+ private_key,
+ cryptography.hazmat.primitives.hashes.SHA256(),
+ _cryptography_backend
+ )
+
+ # Process challenge
+ if challenge == 'tls-alpn-01':
+ value = base64.b64decode(challenge_data['resource_value'])
+ challenge_certificate = cryptography.x509.CertificateBuilder().subject_name(
+ subject
+ ).issuer_name(
+ issuer
+ ).public_key(
+ private_key.public_key()
+ ).serial_number(
+ cryptography.x509.random_serial_number()
+ ).not_valid_before(
+ not_valid_before
+ ).not_valid_after(
+ not_valid_after
+ ).add_extension(
+ cryptography.x509.SubjectAlternativeName([san]),
+ critical=False,
+ ).add_extension(
+ cryptography.x509.UnrecognizedExtension(
+ cryptography.x509.ObjectIdentifier("1.3.6.1.5.5.7.1.31"),
+ encode_octet_string(value),
+ ),
+ critical=True,
+ ).sign(
+ private_key,
+ cryptography.hazmat.primitives.hashes.SHA256(),
+ _cryptography_backend
+ )
+
+ module.exit_json(
+ changed=True,
+ domain=domain,
+ identifier_type=identifier_type,
+ identifier=identifier,
+ challenge_certificate=challenge_certificate.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM),
+ regular_certificate=regular_certificate.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM)
+ )
+ except ModuleFailException as e:
+ e.do_fail(module)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/acme_inspect.py b/ansible_collections/community/crypto/plugins/modules/acme_inspect.py
new file mode 100644
index 00000000..d5c96b72
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/acme_inspect.py
@@ -0,0 +1,325 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2018 Felix Fontein (@felixfontein)
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: acme_inspect
+author: "Felix Fontein (@felixfontein)"
+short_description: Send direct requests to an ACME server
+description:
+ - "Allows to send direct requests to an ACME server with the
+ L(ACME protocol,https://tools.ietf.org/html/rfc8555),
+ which is supported by CAs such as L(Let's Encrypt,https://letsencrypt.org/)."
+ - "This module can be used to debug failed certificate request attempts,
+ for example when M(community.crypto.acme_certificate) fails or encounters a problem which
+ you wish to investigate."
+ - "The module can also be used to directly access features of an ACME servers
+ which are not yet supported by the Ansible ACME modules."
+notes:
+ - "The I(account_uri) option must be specified for properly authenticated
+ ACME v2 requests (except a C(new-account) request)."
+ - "Using the C(ansible) tool, M(community.crypto.acme_inspect) can be used to directly execute
+ ACME requests without the need of writing a playbook. For example, the
+ following command retrieves the ACME account with ID 1 from Let's Encrypt
+ (assuming C(/path/to/key) is the correct private account key):
+ C(ansible localhost -m acme_inspect -a \"account_key_src=/path/to/key
+ acme_directory=https://acme-v02.api.letsencrypt.org/directory acme_version=2
+ account_uri=https://acme-v02.api.letsencrypt.org/acme/acct/1 method=get
+ url=https://acme-v02.api.letsencrypt.org/acme/acct/1\")"
+seealso:
+ - name: Automatic Certificate Management Environment (ACME)
+ description: The specification of the ACME protocol (RFC 8555).
+ link: https://tools.ietf.org/html/rfc8555
+ - name: ACME TLS ALPN Challenge Extension
+ description: The specification of the C(tls-alpn-01) challenge (RFC 8737).
+ link: https://www.rfc-editor.org/rfc/rfc8737.html
+extends_documentation_fragment:
+ - community.crypto.acme
+ - community.crypto.attributes
+ - community.crypto.attributes.actiongroup_acme
+attributes:
+ check_mode:
+ support: none
+ diff_mode:
+ support: none
+options:
+ url:
+ description:
+ - "The URL to send the request to."
+ - "Must be specified if I(method) is not C(directory-only)."
+ type: str
+ method:
+ description:
+ - "The method to use to access the given URL on the ACME server."
+ - "The value C(post) executes an authenticated POST request. The content
+ must be specified in the I(content) option."
+ - "The value C(get) executes an authenticated POST-as-GET request for ACME v2,
+ and a regular GET request for ACME v1."
+ - "The value C(directory-only) only retrieves the directory, without doing
+ a request."
+ type: str
+ default: get
+ choices:
+ - get
+ - post
+ - directory-only
+ content:
+ description:
+ - "An encoded JSON object which will be sent as the content if I(method)
+ is C(post)."
+ - "Required when I(method) is C(post), and not allowed otherwise."
+ type: str
+ fail_on_acme_error:
+ description:
+ - "If I(method) is C(post) or C(get), make the module fail in case an ACME
+ error is returned."
+ type: bool
+ default: true
+'''
+
+EXAMPLES = r'''
+- name: Get directory
+ community.crypto.acme_inspect:
+ acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
+ acme_version: 2
+ method: directory-only
+ register: directory
+
+- name: Create an account
+ community.crypto.acme_inspect:
+ acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
+ acme_version: 2
+ account_key_src: /etc/pki/cert/private/account.key
+ url: "{{ directory.newAccount}}"
+ method: post
+ content: '{"termsOfServiceAgreed":true}'
+ register: account_creation
+ # account_creation.headers.location contains the account URI
+ # if creation was successful
+
+- name: Get account information
+ community.crypto.acme_inspect:
+ acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
+ acme_version: 2
+ account_key_src: /etc/pki/cert/private/account.key
+ account_uri: "{{ account_creation.headers.location }}"
+ url: "{{ account_creation.headers.location }}"
+ method: get
+
+- name: Update account contacts
+ community.crypto.acme_inspect:
+ acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
+ acme_version: 2
+ account_key_src: /etc/pki/cert/private/account.key
+ account_uri: "{{ account_creation.headers.location }}"
+ url: "{{ account_creation.headers.location }}"
+ method: post
+ content: '{{ account_info | to_json }}'
+ vars:
+ account_info:
+ # For valid values, see
+ # https://tools.ietf.org/html/rfc8555#section-7.3
+ contact:
+ - mailto:me@example.com
+
+- name: Create certificate order
+ community.crypto.acme_certificate:
+ acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
+ acme_version: 2
+ account_key_src: /etc/pki/cert/private/account.key
+ account_uri: "{{ account_creation.headers.location }}"
+ csr: /etc/pki/cert/csr/sample.com.csr
+ fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
+ challenge: http-01
+ register: certificate_request
+
+# Assume something went wrong. certificate_request.order_uri contains
+# the order URI.
+
+- name: Get order information
+ community.crypto.acme_inspect:
+ acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
+ acme_version: 2
+ account_key_src: /etc/pki/cert/private/account.key
+ account_uri: "{{ account_creation.headers.location }}"
+ url: "{{ certificate_request.order_uri }}"
+ method: get
+ register: order
+
+- name: Get first authz for order
+ community.crypto.acme_inspect:
+ acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
+ acme_version: 2
+ account_key_src: /etc/pki/cert/private/account.key
+ account_uri: "{{ account_creation.headers.location }}"
+ url: "{{ order.output_json.authorizations[0] }}"
+ method: get
+ register: authz
+
+- name: Get HTTP-01 challenge for authz
+ community.crypto.acme_inspect:
+ acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
+ acme_version: 2
+ account_key_src: /etc/pki/cert/private/account.key
+ account_uri: "{{ account_creation.headers.location }}"
+ url: "{{ authz.output_json.challenges | selectattr('type', 'equalto', 'http-01') }}"
+ method: get
+ register: http01challenge
+
+- name: Activate HTTP-01 challenge manually
+ community.crypto.acme_inspect:
+ acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
+ acme_version: 2
+ account_key_src: /etc/pki/cert/private/account.key
+ account_uri: "{{ account_creation.headers.location }}"
+ url: "{{ http01challenge.url }}"
+ method: post
+ content: '{}'
+'''
+
+RETURN = '''
+directory:
+ description: The ACME directory's content
+ returned: always
+ type: dict
+ sample:
+ {
+ "a85k3x9f91A4": "https://community.letsencrypt.org/t/adding-random-entries-to-the-directory/33417",
+ "keyChange": "https://acme-v02.api.letsencrypt.org/acme/key-change",
+ "meta": {
+ "caaIdentities": [
+ "letsencrypt.org"
+ ],
+ "termsOfService": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf",
+ "website": "https://letsencrypt.org"
+ },
+ "newAccount": "https://acme-v02.api.letsencrypt.org/acme/new-acct",
+ "newNonce": "https://acme-v02.api.letsencrypt.org/acme/new-nonce",
+ "newOrder": "https://acme-v02.api.letsencrypt.org/acme/new-order",
+ "revokeCert": "https://acme-v02.api.letsencrypt.org/acme/revoke-cert"
+ }
+headers:
+ description: The request's HTTP headers (with lowercase keys)
+ returned: always
+ type: dict
+ sample:
+ {
+ "boulder-requester": "12345",
+ "cache-control": "max-age=0, no-cache, no-store",
+ "connection": "close",
+ "content-length": "904",
+ "content-type": "application/json",
+ "cookies": {},
+ "cookies_string": "",
+ "date": "Wed, 07 Nov 2018 12:34:56 GMT",
+ "expires": "Wed, 07 Nov 2018 12:44:56 GMT",
+ "link": '<https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf>;rel="terms-of-service"',
+ "msg": "OK (904 bytes)",
+ "pragma": "no-cache",
+ "replay-nonce": "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGH",
+ "server": "nginx",
+ "status": 200,
+ "strict-transport-security": "max-age=604800",
+ "url": "https://acme-v02.api.letsencrypt.org/acme/acct/46161",
+ "x-frame-options": "DENY"
+ }
+output_text:
+ description: The raw text output
+ returned: always
+ type: str
+ sample: "{\\n \\\"id\\\": 12345,\\n \\\"key\\\": {\\n \\\"kty\\\": \\\"RSA\\\",\\n ..."
+output_json:
+ description: The output parsed as JSON
+ returned: if output can be parsed as JSON
+ type: dict
+ sample:
+ - id: 12345
+ - key:
+ - kty: RSA
+ - ...
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_native, to_bytes, to_text
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
+ create_backend,
+ get_default_argspec,
+ ACMEClient,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.acme.errors import (
+ ACMEProtocolException,
+ ModuleFailException,
+)
+
+
+def main():
+ argument_spec = get_default_argspec()
+ argument_spec.update(dict(
+ url=dict(type='str'),
+ method=dict(type='str', choices=['get', 'post', 'directory-only'], default='get'),
+ content=dict(type='str'),
+ fail_on_acme_error=dict(type='bool', default=True),
+ ))
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ mutually_exclusive=(
+ ['account_key_src', 'account_key_content'],
+ ),
+ required_if=(
+ ['method', 'get', ['url']],
+ ['method', 'post', ['url', 'content']],
+ ['method', 'get', ['account_key_src', 'account_key_content'], True],
+ ['method', 'post', ['account_key_src', 'account_key_content'], True],
+ ),
+ )
+ backend = create_backend(module, False)
+
+ result = dict()
+ changed = False
+ try:
+ # Get hold of ACMEClient and ACMEAccount objects (includes directory)
+ client = ACMEClient(module, backend)
+ method = module.params['method']
+ result['directory'] = client.directory.directory
+ # Do we have to do more requests?
+ if method != 'directory-only':
+ url = module.params['url']
+ fail_on_acme_error = module.params['fail_on_acme_error']
+ # Do request
+ if method == 'get':
+ data, info = client.get_request(url, parse_json_result=False, fail_on_error=False)
+ elif method == 'post':
+ changed = True # only POSTs can change
+ data, info = client.send_signed_request(
+ url, to_bytes(module.params['content']), parse_json_result=False, encode_payload=False, fail_on_error=False)
+ # Update results
+ result.update(dict(
+ headers=info,
+ output_text=to_native(data),
+ ))
+ # See if we can parse the result as JSON
+ try:
+ result['output_json'] = module.from_json(to_text(data))
+ except Exception as dummy:
+ pass
+ # Fail if error was returned
+ if fail_on_acme_error and info['status'] >= 400:
+ raise ACMEProtocolException(module, info=info, content=data)
+ # Done!
+ module.exit_json(changed=changed, **result)
+ except ModuleFailException as e:
+ e.do_fail(module, **result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/certificate_complete_chain.py b/ansible_collections/community/crypto/plugins/modules/certificate_complete_chain.py
new file mode 100644
index 00000000..c05718e0
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/certificate_complete_chain.py
@@ -0,0 +1,375 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2018, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: certificate_complete_chain
+author: "Felix Fontein (@felixfontein)"
+short_description: Complete certificate chain given a set of untrusted and root certificates
+description:
+ - "This module completes a given chain of certificates in PEM format by finding
+ intermediate certificates from a given set of certificates, until it finds a root
+ certificate in another given set of certificates."
+ - "This can for example be used to find the root certificate for a certificate chain
+ returned by M(community.crypto.acme_certificate)."
+ - "Note that this module does I(not) check for validity of the chains. It only
+ checks that issuer and subject match, and that the signature is correct. It
+ ignores validity dates and key usage completely. If you need to verify that a
+ generated chain is valid, please use C(openssl verify ...)."
+requirements:
+ - "cryptography >= 1.5"
+extends_documentation_fragment:
+ - community.crypto.attributes
+attributes:
+ check_mode:
+ support: full
+ details:
+ - This action does not modify state.
+ diff_mode:
+ support: N/A
+ details:
+ - This action does not modify state.
+options:
+ input_chain:
+ description:
+ - A concatenated set of certificates in PEM format forming a chain.
+ - The module will try to complete this chain.
+ type: str
+ required: true
+ root_certificates:
+ description:
+ - "A list of filenames or directories."
+ - "A filename is assumed to point to a file containing one or more certificates
+ in PEM format. All certificates in this file will be added to the set of
+ root certificates."
+ - "If a directory name is given, all files in the directory and its
+ subdirectories will be scanned and tried to be parsed as concatenated
+ certificates in PEM format."
+ - "Symbolic links will be followed."
+ type: list
+ elements: path
+ required: true
+ intermediate_certificates:
+ description:
+ - "A list of filenames or directories."
+ - "A filename is assumed to point to a file containing one or more certificates
+ in PEM format. All certificates in this file will be added to the set of
+ root certificates."
+ - "If a directory name is given, all files in the directory and its
+ subdirectories will be scanned and tried to be parsed as concatenated
+ certificates in PEM format."
+ - "Symbolic links will be followed."
+ type: list
+ elements: path
+ default: []
+'''
+
+
+EXAMPLES = '''
+# Given a leaf certificate for www.ansible.com and one or more intermediate
+# certificates, finds the associated root certificate.
+- name: Find root certificate
+ community.crypto.certificate_complete_chain:
+ input_chain: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com-fullchain.pem') }}"
+ root_certificates:
+ - /etc/ca-certificates/
+ register: www_ansible_com
+- name: Write root certificate to disk
+ copy:
+ dest: /etc/ssl/csr/www.ansible.com-root.pem
+ content: "{{ www_ansible_com.root }}"
+
+# Given a leaf certificate for www.ansible.com, and a list of intermediate
+# certificates, finds the associated root certificate.
+- name: Find root certificate
+ community.crypto.certificate_complete_chain:
+ input_chain: "{{ lookup('file', '/etc/ssl/csr/www.ansible.com.pem') }}"
+ intermediate_certificates:
+ - /etc/ssl/csr/www.ansible.com-chain.pem
+ root_certificates:
+ - /etc/ca-certificates/
+ register: www_ansible_com
+- name: Write complete chain to disk
+ copy:
+ dest: /etc/ssl/csr/www.ansible.com-completechain.pem
+ content: "{{ ''.join(www_ansible_com.complete_chain) }}"
+- name: Write root chain (intermediates and root) to disk
+ copy:
+ dest: /etc/ssl/csr/www.ansible.com-rootchain.pem
+ content: "{{ ''.join(www_ansible_com.chain) }}"
+'''
+
+
+RETURN = '''
+root:
+ description:
+ - "The root certificate in PEM format."
+ returned: success
+ type: str
+chain:
+ description:
+ - "The chain added to the given input chain. Includes the root certificate."
+ - "Returned as a list of PEM certificates."
+ returned: success
+ type: list
+ elements: str
+complete_chain:
+ description:
+ - "The completed chain, including leaf, all intermediates, and root."
+ - "Returned as a list of PEM certificates."
+ returned: success
+ type: list
+ elements: str
+'''
+
+import os
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.common.text.converters import to_bytes
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
+ split_pem_list,
+)
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ import cryptography.exceptions
+ import cryptography.hazmat.backends
+ import cryptography.hazmat.primitives.serialization
+ import cryptography.hazmat.primitives.asymmetric.rsa
+ import cryptography.hazmat.primitives.asymmetric.ec
+ import cryptography.hazmat.primitives.asymmetric.padding
+ import cryptography.hazmat.primitives.hashes
+ import cryptography.hazmat.primitives.asymmetric.utils
+ import cryptography.x509
+ import cryptography.x509.oid
+ HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.5'))
+ _cryptography_backend = cryptography.hazmat.backends.default_backend()
+except ImportError as dummy:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ HAS_CRYPTOGRAPHY = False
+
+
+class Certificate(object):
+ '''
+ Stores PEM with parsed certificate.
+ '''
+ def __init__(self, pem, cert):
+ if not (pem.endswith('\n') or pem.endswith('\r')):
+ pem = pem + '\n'
+ self.pem = pem
+ self.cert = cert
+
+
+def is_parent(module, cert, potential_parent):
+ '''
+ Tests whether the given certificate has been issued by the potential parent certificate.
+ '''
+ # Check issuer
+ if cert.cert.issuer != potential_parent.cert.subject:
+ return False
+ # Check signature
+ public_key = potential_parent.cert.public_key()
+ try:
+ if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
+ public_key.verify(
+ cert.cert.signature,
+ cert.cert.tbs_certificate_bytes,
+ cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15(),
+ cert.cert.signature_hash_algorithm
+ )
+ elif isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
+ public_key.verify(
+ cert.cert.signature,
+ cert.cert.tbs_certificate_bytes,
+ cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cert.cert.signature_hash_algorithm),
+ )
+ else:
+ # Unknown public key type
+ module.warn('Unknown public key type "{0}"'.format(public_key))
+ return False
+ return True
+ except cryptography.exceptions.InvalidSignature as dummy:
+ return False
+ except cryptography.exceptions.UnsupportedAlgorithm as dummy:
+ module.warn('Unsupported algorithm "{0}"'.format(cert.cert.signature_hash_algorithm))
+ return False
+ except Exception as e:
+ module.fail_json(msg='Unknown error on signature validation: {0}'.format(e))
+
+
+def parse_PEM_list(module, text, source, fail_on_error=True):
+ '''
+ Parse concatenated PEM certificates. Return list of ``Certificate`` objects.
+ '''
+ result = []
+ for cert_pem in split_pem_list(text):
+ # Try to load PEM certificate
+ try:
+ cert = cryptography.x509.load_pem_x509_certificate(to_bytes(cert_pem), _cryptography_backend)
+ result.append(Certificate(cert_pem, cert))
+ except Exception as e:
+ msg = 'Cannot parse certificate #{0} from {1}: {2}'.format(len(result) + 1, source, e)
+ if fail_on_error:
+ module.fail_json(msg=msg)
+ else:
+ module.warn(msg)
+ return result
+
+
+def load_PEM_list(module, path, fail_on_error=True):
+ '''
+ Load concatenated PEM certificates from file. Return list of ``Certificate`` objects.
+ '''
+ try:
+ with open(path, "rb") as f:
+ return parse_PEM_list(module, f.read().decode('utf-8'), source=path, fail_on_error=fail_on_error)
+ except Exception as e:
+ msg = 'Cannot read certificate file {0}: {1}'.format(path, e)
+ if fail_on_error:
+ module.fail_json(msg=msg)
+ else:
+ module.warn(msg)
+ return []
+
+
+class CertificateSet(object):
+ '''
+ Stores a set of certificates. Allows to search for parent (issuer of a certificate).
+ '''
+
+ def __init__(self, module):
+ self.module = module
+ self.certificates = set()
+ self.certificates_by_issuer = dict()
+ self.certificate_by_cert = dict()
+
+ def _load_file(self, path):
+ certs = load_PEM_list(self.module, path, fail_on_error=False)
+ for cert in certs:
+ self.certificates.add(cert)
+ if cert.cert.subject not in self.certificates_by_issuer:
+ self.certificates_by_issuer[cert.cert.subject] = []
+ self.certificates_by_issuer[cert.cert.subject].append(cert)
+ self.certificate_by_cert[cert.cert] = cert
+
+ def load(self, path):
+ '''
+ Load lists of PEM certificates from a file or a directory.
+ '''
+ b_path = to_bytes(path, errors='surrogate_or_strict')
+ if os.path.isdir(b_path):
+ for directory, dummy, files in os.walk(b_path, followlinks=True):
+ for file in files:
+ self._load_file(os.path.join(directory, file))
+ else:
+ self._load_file(b_path)
+
+ def find_parent(self, cert):
+ '''
+ Search for the parent (issuer) of a certificate. Return ``None`` if none was found.
+ '''
+ potential_parents = self.certificates_by_issuer.get(cert.cert.issuer, [])
+ for potential_parent in potential_parents:
+ if is_parent(self.module, cert, potential_parent):
+ return potential_parent
+ return None
+
+
+def format_cert(cert):
+ '''
+ Return human readable representation of certificate for error messages.
+ '''
+ return str(cert.cert)
+
+
+def check_cycle(module, occured_certificates, next):
+ '''
+ Make sure that next is not in occured_certificates so far, and add it.
+ '''
+ next_cert = next.cert
+ if next_cert in occured_certificates:
+ module.fail_json(msg='Found cycle while building certificate chain')
+ occured_certificates.add(next_cert)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ input_chain=dict(type='str', required=True),
+ root_certificates=dict(type='list', required=True, elements='path'),
+ intermediate_certificates=dict(type='list', default=[], elements='path'),
+ ),
+ supports_check_mode=True,
+ )
+
+ if not HAS_CRYPTOGRAPHY:
+ module.fail_json(msg=missing_required_lib('cryptography >= 1.5'), exception=CRYPTOGRAPHY_IMP_ERR)
+
+ # Load chain
+ chain = parse_PEM_list(module, module.params['input_chain'], source='input chain')
+ if len(chain) == 0:
+ module.fail_json(msg='Input chain must contain at least one certificate')
+
+ # Check chain
+ for i, parent in enumerate(chain):
+ if i > 0:
+ if not is_parent(module, chain[i - 1], parent):
+ module.fail_json(msg=('Cannot verify input chain: certificate #{2}: {3} is not issuer ' +
+ 'of certificate #{0}: {1}').format(i, format_cert(chain[i - 1]), i + 1, format_cert(parent)))
+
+ # Load intermediate certificates
+ intermediates = CertificateSet(module)
+ for path in module.params['intermediate_certificates']:
+ intermediates.load(path)
+
+ # Load root certificates
+ roots = CertificateSet(module)
+ for path in module.params['root_certificates']:
+ roots.load(path)
+
+ # Try to complete chain
+ current = chain[-1]
+ completed = []
+ occured_certificates = set([cert.cert for cert in chain])
+ if current.cert in roots.certificate_by_cert:
+ # Do not try to complete the chain when it's already ending with a root certificate
+ current = None
+ while current:
+ root = roots.find_parent(current)
+ if root:
+ check_cycle(module, occured_certificates, root)
+ completed.append(root)
+ break
+ intermediate = intermediates.find_parent(current)
+ if intermediate:
+ check_cycle(module, occured_certificates, intermediate)
+ completed.append(intermediate)
+ current = intermediate
+ else:
+ module.fail_json(msg='Cannot complete chain. Stuck at certificate {0}'.format(format_cert(current)))
+
+ # Return results
+ complete_chain = chain + completed
+ module.exit_json(
+ changed=False,
+ root=complete_chain[-1].pem,
+ chain=[cert.pem for cert in completed],
+ complete_chain=[cert.pem for cert in complete_chain],
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/crypto_info.py b/ansible_collections/community/crypto/plugins/modules/crypto_info.py
new file mode 100644
index 00000000..1988eb32
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/crypto_info.py
@@ -0,0 +1,337 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: crypto_info
+author: "Felix Fontein (@felixfontein)"
+short_description: Retrieve cryptographic capabilities
+version_added: 2.1.0
+description:
+ - Retrieve information on cryptographic capabilities.
+ - The current version retrieves information on the L(Python cryptography library, https://cryptography.io/) available to
+ Ansible modules, and on the OpenSSL binary C(openssl) found in the path.
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - community.crypto.attributes.info_module
+options: {}
+'''
+
+EXAMPLES = r'''
+- name: Retrieve information
+ community.crypto.crypto_info:
+ account_key_src: /etc/pki/cert/private/account.key
+ register: crypto_information
+
+- name: Show retrieved information
+ ansible.builtin.debug:
+ var: crypto_information
+'''
+
+RETURN = r'''
+python_cryptography_installed:
+ description: Whether the L(Python cryptography library, https://cryptography.io/) is installed.
+ returned: always
+ type: bool
+ sample: true
+
+python_cryptography_import_error:
+ description: Import error when trying to import the L(Python cryptography library, https://cryptography.io/).
+ returned: when I(python_cryptography_installed=false)
+ type: str
+
+python_cryptography_capabilities:
+ description: Information on the installed L(Python cryptography library, https://cryptography.io/).
+ returned: when I(python_cryptography_installed=true)
+ type: dict
+ contains:
+ version:
+ description: The library version.
+ type: str
+ curves:
+ description:
+ - List of all supported elliptic curves.
+ - Theoretically this should be non-empty for version 0.5 and higher, depending on the libssl version used.
+ type: list
+ elements: str
+ has_ec:
+ description:
+ - Whether elliptic curves are supported.
+ - Theoretically this should be the case for version 0.5 and higher, depending on the libssl version used.
+ type: bool
+ has_ec_sign:
+ description:
+ - Whether signing with elliptic curves is supported.
+ - Theoretically this should be the case for version 1.5 and higher, depending on the libssl version used.
+ type: bool
+ has_ed25519:
+ description:
+ - Whether Ed25519 keys are supported.
+ - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used.
+ type: bool
+ has_ed25519_sign:
+ description:
+ - Whether signing with Ed25519 keys is supported.
+ - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used.
+ type: bool
+ has_ed448:
+ description:
+ - Whether Ed448 keys are supported.
+ - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used.
+ type: bool
+ has_ed448_sign:
+ description:
+ - Whether signing with Ed448 keys is supported.
+ - Theoretically this should be the case for version 2.6 and higher, depending on the libssl version used.
+ type: bool
+ has_dsa:
+ description:
+ - Whether DSA keys are supported.
+ - Theoretically this should be the case for version 0.5 and higher.
+ type: bool
+ has_dsa_sign:
+ description:
+ - Whether signing with DSA keys is supported.
+ - Theoretically this should be the case for version 1.5 and higher.
+ type: bool
+ has_rsa:
+ description:
+ - Whether RSA keys are supported.
+ - Theoretically this should be the case for version 0.5 and higher.
+ type: bool
+ has_rsa_sign:
+ description:
+ - Whether signing with RSA keys is supported.
+ - Theoretically this should be the case for version 1.4 and higher.
+ type: bool
+ has_x25519:
+ description:
+ - Whether X25519 keys are supported.
+ - Theoretically this should be the case for version 2.0 and higher, depending on the libssl version used.
+ type: bool
+ has_x25519_serialization:
+ description:
+ - Whether serialization of X25519 keys is supported.
+ - Theoretically this should be the case for version 2.5 and higher, depending on the libssl version used.
+ type: bool
+ has_x448:
+ description:
+ - Whether X448 keys are supported.
+ - Theoretically this should be the case for version 2.5 and higher, depending on the libssl version used.
+ type: bool
+
+openssl_present:
+ description: Whether the OpenSSL binary C(openssl) is installed and can be found in the PATH.
+ returned: always
+ type: bool
+ sample: true
+
+openssl:
+ description: Information on the installed OpenSSL binary.
+ returned: when I(openssl_present=true)
+ type: dict
+ contains:
+ path:
+ description: Path of the OpenSSL binary.
+ type: str
+ sample: /usr/bin/openssl
+ version:
+ description: The OpenSSL version.
+ type: str
+ sample: 1.1.1m
+ version_output:
+ description: The complete output of C(openssl version).
+ type: str
+ sample: 'OpenSSL 1.1.1m 14 Dec 2021\n'
+'''
+
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ CRYPTOGRAPHY_HAS_EC,
+ CRYPTOGRAPHY_HAS_EC_SIGN,
+ CRYPTOGRAPHY_HAS_ED25519,
+ CRYPTOGRAPHY_HAS_ED25519_SIGN,
+ CRYPTOGRAPHY_HAS_ED448,
+ CRYPTOGRAPHY_HAS_ED448_SIGN,
+ CRYPTOGRAPHY_HAS_DSA,
+ CRYPTOGRAPHY_HAS_DSA_SIGN,
+ CRYPTOGRAPHY_HAS_RSA,
+ CRYPTOGRAPHY_HAS_RSA_SIGN,
+ CRYPTOGRAPHY_HAS_X25519,
+ CRYPTOGRAPHY_HAS_X25519_FULL,
+ CRYPTOGRAPHY_HAS_X448,
+ HAS_CRYPTOGRAPHY,
+)
+
+try:
+ import cryptography
+ from cryptography.exceptions import UnsupportedAlgorithm
+except ImportError:
+ UnsupportedAlgorithm = Exception
+ CRYPTOGRAPHY_VERSION = None
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+else:
+ CRYPTOGRAPHY_VERSION = cryptography.__version__
+ CRYPTOGRAPHY_IMP_ERR = None
+
+
+CURVES = (
+ ('secp224r1', 'SECP224R1'),
+ ('secp256k1', 'SECP256K1'),
+ ('secp256r1', 'SECP256R1'),
+ ('secp384r1', 'SECP384R1'),
+ ('secp521r1', 'SECP521R1'),
+ ('secp192r1', 'SECP192R1'),
+ ('sect163k1', 'SECT163K1'),
+ ('sect163r2', 'SECT163R2'),
+ ('sect233k1', 'SECT233K1'),
+ ('sect233r1', 'SECT233R1'),
+ ('sect283k1', 'SECT283K1'),
+ ('sect283r1', 'SECT283R1'),
+ ('sect409k1', 'SECT409K1'),
+ ('sect409r1', 'SECT409R1'),
+ ('sect571k1', 'SECT571K1'),
+ ('sect571r1', 'SECT571R1'),
+ ('brainpoolP256r1', 'BrainpoolP256R1'),
+ ('brainpoolP384r1', 'BrainpoolP384R1'),
+ ('brainpoolP512r1', 'BrainpoolP512R1'),
+)
+
+
+def add_crypto_information(module):
+ result = {}
+ result['python_cryptography_installed'] = HAS_CRYPTOGRAPHY
+ if not HAS_CRYPTOGRAPHY:
+ result['python_cryptography_import_error'] = CRYPTOGRAPHY_IMP_ERR
+ return result
+
+ has_ed25519 = CRYPTOGRAPHY_HAS_ED25519
+ if has_ed25519:
+ try:
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
+ Ed25519PrivateKey.from_private_bytes(b'')
+ except ValueError:
+ pass
+ except UnsupportedAlgorithm:
+ has_ed25519 = False
+
+ has_ed448 = CRYPTOGRAPHY_HAS_ED448
+ if has_ed448:
+ try:
+ from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey
+ Ed448PrivateKey.from_private_bytes(b'')
+ except ValueError:
+ pass
+ except UnsupportedAlgorithm:
+ has_ed448 = False
+
+ has_x25519 = CRYPTOGRAPHY_HAS_X25519
+ if has_x25519:
+ try:
+ from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey
+ if CRYPTOGRAPHY_HAS_X25519_FULL:
+ X25519PrivateKey.from_private_bytes(b'')
+ else:
+ # Some versions do not support serialization and deserialization - use generate() instead
+ X25519PrivateKey.generate()
+ except ValueError:
+ pass
+ except UnsupportedAlgorithm:
+ has_x25519 = False
+
+ has_x448 = CRYPTOGRAPHY_HAS_X448
+ if has_x448:
+ try:
+ from cryptography.hazmat.primitives.asymmetric.x448 import X448PrivateKey
+ X448PrivateKey.from_private_bytes(b'')
+ except ValueError:
+ pass
+ except UnsupportedAlgorithm:
+ has_x448 = False
+
+ curves = []
+ if CRYPTOGRAPHY_HAS_EC:
+ import cryptography.hazmat.backends
+ import cryptography.hazmat.primitives.asymmetric.ec
+
+ backend = cryptography.hazmat.backends.default_backend()
+ for curve_name, constructor_name in CURVES:
+ ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(constructor_name)
+ if ecclass:
+ try:
+ cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(curve=ecclass(), backend=backend)
+ curves.append(curve_name)
+ except UnsupportedAlgorithm:
+ pass
+
+ info = {
+ 'version': CRYPTOGRAPHY_VERSION,
+ 'curves': curves,
+ 'has_ec': CRYPTOGRAPHY_HAS_EC,
+ 'has_ec_sign': CRYPTOGRAPHY_HAS_EC_SIGN,
+ 'has_ed25519': has_ed25519,
+ 'has_ed25519_sign': has_ed25519 and CRYPTOGRAPHY_HAS_ED25519_SIGN,
+ 'has_ed448': has_ed448,
+ 'has_ed448_sign': has_ed448 and CRYPTOGRAPHY_HAS_ED448_SIGN,
+ 'has_dsa': CRYPTOGRAPHY_HAS_DSA,
+ 'has_dsa_sign': CRYPTOGRAPHY_HAS_DSA_SIGN,
+ 'has_rsa': CRYPTOGRAPHY_HAS_RSA,
+ 'has_rsa_sign': CRYPTOGRAPHY_HAS_RSA_SIGN,
+ 'has_x25519': has_x25519,
+ 'has_x25519_serialization': has_x25519 and CRYPTOGRAPHY_HAS_X25519_FULL,
+ 'has_x448': has_x448,
+ }
+ result['python_cryptography_capabilities'] = info
+ return result
+
+
+def add_openssl_information(module):
+ openssl_binary = module.get_bin_path('openssl')
+ result = {
+ 'openssl_present': openssl_binary is not None,
+ }
+ if openssl_binary is None:
+ return result
+
+ openssl_result = {
+ 'path': openssl_binary,
+ }
+ result['openssl'] = openssl_result
+
+ rc, out, err = module.run_command([openssl_binary, 'version'])
+ if rc == 0:
+ openssl_result['version_output'] = out
+ parts = out.split(None, 2)
+ if len(parts) > 1:
+ openssl_result['version'] = parts[1]
+
+ return result
+
+
+INFO_FUNCTIONS = (
+ add_crypto_information,
+ add_openssl_information,
+)
+
+
+def main():
+ module = AnsibleModule(argument_spec={}, supports_check_mode=True)
+ result = {}
+ for fn in INFO_FUNCTIONS:
+ result.update(fn(module))
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/ecs_certificate.py b/ansible_collections/community/crypto/plugins/modules/ecs_certificate.py
new file mode 100644
index 00000000..b19b86f5
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/ecs_certificate.py
@@ -0,0 +1,966 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c), Entrust Datacard Corporation, 2019
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: ecs_certificate
+author:
+ - Chris Trufan (@ctrufan)
+short_description: Request SSL/TLS certificates with the Entrust Certificate Services (ECS) API
+description:
+ - Create, reissue, and renew certificates with the Entrust Certificate Services (ECS) API.
+ - Requires credentials for the L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API.
+ - In order to request a certificate, the domain and organization used in the certificate signing request must be already
+ validated in the ECS system. It is I(not) the responsibility of this module to perform those steps.
+notes:
+ - C(path) must be specified as the output location of the certificate.
+requirements:
+ - cryptography >= 1.6
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+ - community.crypto.ecs_credential
+attributes:
+ check_mode:
+ support: partial
+ details:
+ - Check mode is only supported if I(request_type=new).
+ diff_mode:
+ support: none
+ safe_file_operations:
+ support: full
+options:
+ backup:
+ description:
+ - Whether a backup should be made for the certificate in I(path).
+ type: bool
+ default: false
+ force:
+ description:
+ - If force is used, a certificate is requested regardless of whether I(path) points to an existing valid certificate.
+ - If C(request_type=renew), a forced renew will fail if the certificate being renewed has been issued within the past 30 days, regardless of the
+ value of I(remaining_days) or the return value of I(cert_days) - the ECS API does not support the "renew" operation for certificates that are not
+ at least 30 days old.
+ type: bool
+ default: false
+ path:
+ description:
+ - The destination path for the generated certificate as a PEM encoded cert.
+ - If the certificate at this location is not an Entrust issued certificate, a new certificate will always be requested even if the current
+ certificate is technically valid.
+ - If there is already an Entrust certificate at this location, whether it is replaced is depends on the I(remaining_days) calculation.
+ - If an existing certificate is being replaced (see I(remaining_days), I(force), and I(tracking_id)), whether a new certificate is requested
+ or the existing certificate is renewed or reissued is based on I(request_type).
+ type: path
+ required: true
+ full_chain_path:
+ description:
+ - The destination path for the full certificate chain of the certificate, intermediates, and roots.
+ type: path
+ csr:
+ description:
+ - Base-64 encoded Certificate Signing Request (CSR). I(csr) is accepted with or without PEM formatting around the Base-64 string.
+ - If no I(csr) is provided when C(request_type=reissue) or C(request_type=renew), the certificate will be generated with the same public key as
+ the certificate being renewed or reissued.
+ - If I(subject_alt_name) is specified, it will override the subject alternate names in the CSR.
+ - If I(eku) is specified, it will override the extended key usage in the CSR.
+ - If I(ou) is specified, it will override the organizational units "ou=" present in the subject distinguished name of the CSR, if any.
+ - The organization "O=" field from the CSR will not be used. It will be replaced in the issued certificate by I(org) if present, and if not present,
+ the organization tied to I(client_id).
+ type: str
+ tracking_id:
+ description:
+ - The tracking ID of the certificate to reissue or renew.
+ - I(tracking_id) is invalid if C(request_type=new) or C(request_type=validate_only).
+ - If there is a certificate present in I(path) and it is an ECS certificate, I(tracking_id) will be ignored.
+ - If there is no certificate present in I(path) or there is but it is from another provider, the certificate represented by I(tracking_id) will
+ be renewed or reissued and saved to I(path).
+ - If there is no certificate present in I(path) and the I(force) and I(remaining_days) parameters do not indicate a new certificate is needed,
+ the certificate referenced by I(tracking_id) certificate will be saved to I(path).
+ - This can be used when a known certificate is not currently present on a server, but you want to renew or reissue it to be managed by an ansible
+ playbook. For example, if you specify C(request_type=renew), I(tracking_id) of an issued certificate, and I(path) to a file that does not exist,
+ the first run of a task will download the certificate specified by I(tracking_id) (assuming it is still valid). Future runs of the task will
+ (if applicable - see I(force) and I(remaining_days)) renew the certificate now present in I(path).
+ type: int
+ remaining_days:
+ description:
+ - The number of days the certificate must have left being valid. If C(cert_days < remaining_days) then a new certificate will be
+ obtained using I(request_type).
+ - If C(request_type=renew), a renewal will fail if the certificate being renewed has been issued within the past 30 days, so do not set a
+ I(remaining_days) value that is within 30 days of the full lifetime of the certificate being acted upon.
+ - For exmaple, if you are requesting Certificates with a 90 day lifetime, do not set I(remaining_days) to a value C(60) or higher).
+ - The I(force) option may be used to ensure that a new certificate is always obtained.
+ type: int
+ default: 30
+ request_type:
+ description:
+ - The operation performed if I(tracking_id) references a valid certificate to reissue, or there is already a certificate present in I(path) but
+ either I(force) is specified or C(cert_days < remaining_days).
+ - Specifying C(request_type=validate_only) means the request will be validated against the ECS API, but no certificate will be issued.
+ - Specifying C(request_type=new) means a certificate request will always be submitted and a new certificate issued.
+ - Specifying C(request_type=renew) means that an existing certificate (specified by I(tracking_id) if present, otherwise I(path)) will be renewed.
+ If there is no certificate to renew, a new certificate is requested.
+ - Specifying C(request_type=reissue) means that an existing certificate (specified by I(tracking_id) if present, otherwise I(path)) will be
+ reissued.
+ If there is no certificate to reissue, a new certificate is requested.
+ - If a certificate was issued within the past 30 days, the C(renew) operation is not a valid operation and will fail.
+ - Note that C(reissue) is an operation that will result in the revocation of the certificate that is reissued, be cautious with its use.
+ - I(check_mode) is only supported if C(request_type=new)
+ - For example, setting C(request_type=renew) and C(remaining_days=30) and pointing to the same certificate on multiple playbook runs means that on
+ the first run new certificate will be requested. It will then be left along on future runs until it is within 30 days of expiry, then the
+ ECS "renew" operation will be performed.
+ type: str
+ choices: [ 'new', 'renew', 'reissue', 'validate_only']
+ default: new
+ cert_type:
+ description:
+ - Specify the type of certificate requested.
+ - If a certificate is being reissued or renewed, this parameter is ignored, and the C(cert_type) of the initial certificate is used.
+ type: str
+ choices: [ 'STANDARD_SSL', 'ADVANTAGE_SSL', 'UC_SSL', 'EV_SSL', 'WILDCARD_SSL', 'PRIVATE_SSL', 'PD_SSL', 'CODE_SIGNING', 'EV_CODE_SIGNING',
+ 'CDS_INDIVIDUAL', 'CDS_GROUP', 'CDS_ENT_LITE', 'CDS_ENT_PRO', 'SMIME_ENT' ]
+ subject_alt_name:
+ description:
+ - The subject alternative name identifiers, as an array of values (applies to I(cert_type) with a value of C(STANDARD_SSL), C(ADVANTAGE_SSL),
+ C(UC_SSL), C(EV_SSL), C(WILDCARD_SSL), C(PRIVATE_SSL), and C(PD_SSL)).
+ - If you are requesting a new SSL certificate, and you pass a I(subject_alt_name) parameter, any SAN names in the CSR are ignored.
+ If no subjectAltName parameter is passed, the SAN names in the CSR are used.
+ - See I(request_type) to understand more about SANs during reissues and renewals.
+ - In the case of certificates of type C(STANDARD_SSL) certificates, if the CN of the certificate is <domain>.<tld> only the www.<domain>.<tld> value
+ is accepted. If the CN of the certificate is www.<domain>.<tld> only the <domain>.<tld> value is accepted.
+ type: list
+ elements: str
+ eku:
+ description:
+ - If specified, overrides the key usage in the I(csr).
+ type: str
+ choices: [ SERVER_AUTH, CLIENT_AUTH, SERVER_AND_CLIENT_AUTH ]
+ ct_log:
+ description:
+ - In compliance with browser requirements, this certificate may be posted to the Certificate Transparency (CT) logs. This is a best practice
+ technique that helps domain owners monitor certificates issued to their domains. Note that not all certificates are eligible for CT logging.
+ - If I(ct_log) is not specified, the certificate uses the account default.
+ - If I(ct_log) is specified and the account settings allow it, I(ct_log) overrides the account default.
+ - If I(ct_log) is set to C(false), but the account settings are set to "always log", the certificate generation will fail.
+ type: bool
+ client_id:
+ description:
+ - The client ID to submit the Certificate Signing Request under.
+ - If no client ID is specified, the certificate will be submitted under the primary client with ID of 1.
+ - When using a client other than the primary client, the I(org) parameter cannot be specified.
+ - The issued certificate will have an organization value in the subject distinguished name represented by the client.
+ type: int
+ default: 1
+ org:
+ description:
+ - Organization "O=" to include in the certificate.
+ - If I(org) is not specified, the organization from the client represented by I(client_id) is used.
+ - Unless the I(cert_type) is C(PD_SSL), this field may not be specified if the value of I(client_id) is not "1" (the primary client).
+ non-primary clients, certificates may only be issued with the organization of that client.
+ type: str
+ ou:
+ description:
+ - Organizational unit "OU=" to include in the certificate.
+ - I(ou) behavior is dependent on whether organizational units are enabled for your account. If organizational unit support is disabled for your
+ account, organizational units from the I(csr) and the I(ou) parameter are ignored.
+ - If both I(csr) and I(ou) are specified, the value in I(ou) will override the OU fields present in the subject distinguished name in the I(csr)
+ - If neither I(csr) nor I(ou) are specified for a renew or reissue operation, the OU fields in the initial certificate are reused.
+ - An invalid OU from I(csr) is ignored, but any invalid organizational units in I(ou) will result in an error indicating "Unapproved OU". The I(ou)
+ parameter can be used to force failure if an unapproved organizational unit is provided.
+ - A maximum of one OU may be specified for current products. Multiple OUs are reserved for future products.
+ type: list
+ elements: str
+ end_user_key_storage_agreement:
+ description:
+ - The end user of the Code Signing certificate must generate and store the private key for this request on cryptographically secure
+ hardware to be compliant with the Entrust CSP and Subscription agreement. If requesting a certificate of type C(CODE_SIGNING) or
+ C(EV_CODE_SIGNING), you must set I(end_user_key_storage_agreement) to true if and only if you acknowledge that you will inform the user of this
+ requirement.
+ - Applicable only to I(cert_type) of values C(CODE_SIGNING) and C(EV_CODE_SIGNING).
+ type: bool
+ tracking_info:
+ description: Free form tracking information to attach to the record for the certificate.
+ type: str
+ requester_name:
+ description: The requester name to associate with certificate tracking information.
+ type: str
+ required: true
+ requester_email:
+ description: The requester email to associate with certificate tracking information and receive delivery and expiry notices for the certificate.
+ type: str
+ required: true
+ requester_phone:
+ description: The requester phone number to associate with certificate tracking information.
+ type: str
+ required: true
+ additional_emails:
+ description: A list of additional email addresses to receive the delivery notice and expiry notification for the certificate.
+ type: list
+ elements: str
+ custom_fields:
+ description:
+ - Mapping of custom fields to associate with the certificate request and certificate.
+ - Only supported if custom fields are enabled for your account.
+ - Each custom field specified must be a custom field you have defined for your account.
+ type: dict
+ suboptions:
+ text1:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text2:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text3:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text4:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text5:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text6:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text7:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text8:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text9:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text10:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text11:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text12:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text13:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text14:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ text15:
+ description: Custom text field (maximum 500 characters)
+ type: str
+ number1:
+ description: Custom number field.
+ type: float
+ number2:
+ description: Custom number field.
+ type: float
+ number3:
+ description: Custom number field.
+ type: float
+ number4:
+ description: Custom number field.
+ type: float
+ number5:
+ description: Custom number field.
+ type: float
+ date1:
+ description: Custom date field.
+ type: str
+ date2:
+ description: Custom date field.
+ type: str
+ date3:
+ description: Custom date field.
+ type: str
+ date4:
+ description: Custom date field.
+ type: str
+ date5:
+ description: Custom date field.
+ type: str
+ email1:
+ description: Custom email field.
+ type: str
+ email2:
+ description: Custom email field.
+ type: str
+ email3:
+ description: Custom email field.
+ type: str
+ email4:
+ description: Custom email field.
+ type: str
+ email5:
+ description: Custom email field.
+ type: str
+ dropdown1:
+ description: Custom dropdown field.
+ type: str
+ dropdown2:
+ description: Custom dropdown field.
+ type: str
+ dropdown3:
+ description: Custom dropdown field.
+ type: str
+ dropdown4:
+ description: Custom dropdown field.
+ type: str
+ dropdown5:
+ description: Custom dropdown field.
+ type: str
+ cert_expiry:
+ description:
+ - The date the certificate should be set to expire, in RFC3339 compliant date or date-time format. For example,
+ C(2020-02-23), C(2020-02-23T15:00:00.05Z).
+ - I(cert_expiry) is only supported for requests of C(request_type=new) or C(request_type=renew). If C(request_type=reissue),
+ I(cert_expiry) will be used for the first certificate issuance, but subsequent issuances will have the same expiry as the initial
+ certificate.
+ - A reissued certificate will always have the same expiry as the original certificate.
+ - Note that only the date (day, month, year) is supported for specifying the expiry date. If you choose to specify an expiry time with the expiry
+ date, the time will be adjusted to Eastern Standard Time (EST). This could have the unintended effect of moving your expiry date to the previous
+ day.
+ - Applies only to accounts with a pooling inventory model.
+ - Only one of I(cert_expiry) or I(cert_lifetime) may be specified.
+ type: str
+ cert_lifetime:
+ description:
+ - The lifetime of the certificate.
+ - Applies to all certificates for accounts with a non-pooling inventory model.
+ - I(cert_lifetime) is only supported for requests of C(request_type=new) or C(request_type=renew). If C(request_type=reissue), I(cert_lifetime) will
+ be used for the first certificate issuance, but subsequent issuances will have the same expiry as the initial certificate.
+ - Applies to certificates of I(cert_type)=C(CDS_INDIVIDUAL, CDS_GROUP, CDS_ENT_LITE, CDS_ENT_PRO, SMIME_ENT) for accounts with a pooling inventory
+ model.
+ - C(P1Y) is a certificate with a 1 year lifetime.
+ - C(P2Y) is a certificate with a 2 year lifetime.
+ - C(P3Y) is a certificate with a 3 year lifetime.
+ - Only one of I(cert_expiry) or I(cert_lifetime) may be specified.
+ type: str
+ choices: [ P1Y, P2Y, P3Y ]
+seealso:
+ - module: community.crypto.openssl_privatekey
+ description: Can be used to create private keys (both for certificates and accounts).
+ - module: community.crypto.openssl_csr
+ description: Can be used to create a Certificate Signing Request (CSR).
+'''
+
+EXAMPLES = r'''
+- name: Request a new certificate from Entrust with bare minimum parameters.
+ Will request a new certificate if current one is valid but within 30
+ days of expiry. If replacing an existing file in path, will back it up.
+ community.crypto.ecs_certificate:
+ backup: true
+ path: /etc/ssl/crt/ansible.com.crt
+ full_chain_path: /etc/ssl/crt/ansible.com.chain.crt
+ csr: /etc/ssl/csr/ansible.com.csr
+ cert_type: EV_SSL
+ requester_name: Jo Doe
+ requester_email: jdoe@ansible.com
+ requester_phone: 555-555-5555
+ entrust_api_user: apiusername
+ entrust_api_key: a^lv*32!cd9LnT
+ entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
+ entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
+
+- name: If there is no certificate present in path, request a new certificate
+ of type EV_SSL. Otherwise, if there is an Entrust managed certificate
+ in path and it is within 63 days of expiration, request a renew of that
+ certificate.
+ community.crypto.ecs_certificate:
+ path: /etc/ssl/crt/ansible.com.crt
+ csr: /etc/ssl/csr/ansible.com.csr
+ cert_type: EV_SSL
+ cert_expiry: '2020-08-20'
+ request_type: renew
+ remaining_days: 63
+ requester_name: Jo Doe
+ requester_email: jdoe@ansible.com
+ requester_phone: 555-555-5555
+ entrust_api_user: apiusername
+ entrust_api_key: a^lv*32!cd9LnT
+ entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
+ entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
+
+- name: If there is no certificate present in path, download certificate
+ specified by tracking_id if it is still valid. Otherwise, if the
+ certificate is within 79 days of expiration, request a renew of that
+ certificate and save it in path. This can be used to "migrate" a
+ certificate to be Ansible managed.
+ community.crypto.ecs_certificate:
+ path: /etc/ssl/crt/ansible.com.crt
+ csr: /etc/ssl/csr/ansible.com.csr
+ tracking_id: 2378915
+ request_type: renew
+ remaining_days: 79
+ entrust_api_user: apiusername
+ entrust_api_key: a^lv*32!cd9LnT
+ entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
+ entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
+
+- name: Force a reissue of the certificate specified by tracking_id.
+ community.crypto.ecs_certificate:
+ path: /etc/ssl/crt/ansible.com.crt
+ force: true
+ tracking_id: 2378915
+ request_type: reissue
+ entrust_api_user: apiusername
+ entrust_api_key: a^lv*32!cd9LnT
+ entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
+ entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
+
+- name: Request a new certificate with an alternative client. Note that the
+ issued certificate will have it's Subject Distinguished Name use the
+ organization details associated with that client, rather than what is
+ in the CSR.
+ community.crypto.ecs_certificate:
+ path: /etc/ssl/crt/ansible.com.crt
+ csr: /etc/ssl/csr/ansible.com.csr
+ client_id: 2
+ requester_name: Jo Doe
+ requester_email: jdoe@ansible.com
+ requester_phone: 555-555-5555
+ entrust_api_user: apiusername
+ entrust_api_key: a^lv*32!cd9LnT
+ entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
+ entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
+
+- name: Request a new certificate with a number of CSR parameters overridden
+ and tracking information
+ community.crypto.ecs_certificate:
+ path: /etc/ssl/crt/ansible.com.crt
+ full_chain_path: /etc/ssl/crt/ansible.com.chain.crt
+ csr: /etc/ssl/csr/ansible.com.csr
+ subject_alt_name:
+ - ansible.testcertificates.com
+ - www.testcertificates.com
+ eku: SERVER_AND_CLIENT_AUTH
+ ct_log: true
+ org: Test Organization Inc.
+ ou:
+ - Administration
+ tracking_info: "Submitted via Ansible"
+ additional_emails:
+ - itsupport@testcertificates.com
+ - jsmith@ansible.com
+ custom_fields:
+ text1: Admin
+ text2: Invoice 25
+ number1: 342
+ date1: '2018-01-01'
+ email1: sales@ansible.testcertificates.com
+ dropdown1: red
+ cert_expiry: '2020-08-15'
+ requester_name: Jo Doe
+ requester_email: jdoe@ansible.com
+ requester_phone: 555-555-5555
+ entrust_api_user: apiusername
+ entrust_api_key: a^lv*32!cd9LnT
+ entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
+ entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
+
+'''
+
+RETURN = '''
+filename:
+ description: The destination path for the generated certificate.
+ returned: changed or success
+ type: str
+ sample: /etc/ssl/crt/www.ansible.com.crt
+backup_file:
+ description: Name of backup file created for the certificate.
+ returned: changed and if I(backup) is C(true)
+ type: str
+ sample: /path/to/www.ansible.com.crt.2019-03-09@11:22~
+backup_full_chain_file:
+ description: Name of the backup file created for the certificate chain.
+ returned: changed and if I(backup) is C(true) and I(full_chain_path) is set.
+ type: str
+ sample: /path/to/ca.chain.crt.2019-03-09@11:22~
+tracking_id:
+ description: The tracking ID to reference and track the certificate in ECS.
+ returned: success
+ type: int
+ sample: 380079
+serial_number:
+ description: The serial number of the issued certificate.
+ returned: success
+ type: int
+ sample: 1235262234164342
+cert_days:
+ description: The number of days the certificate remains valid.
+ returned: success
+ type: int
+ sample: 253
+cert_status:
+ description:
+ - The certificate status in ECS.
+ - 'Current possible values (which may be expanded in the future) are: C(ACTIVE), C(APPROVED), C(DEACTIVATED), C(DECLINED), C(EXPIRED), C(NA),
+ C(PENDING), C(PENDING_QUORUM), C(READY), C(REISSUED), C(REISSUING), C(RENEWED), C(RENEWING), C(REVOKED), C(SUSPENDED)'
+ returned: success
+ type: str
+ sample: ACTIVE
+cert_details:
+ description:
+ - The full response JSON from the Get Certificate call of the ECS API.
+ - 'While the response contents are guaranteed to be forwards compatible with new ECS API releases, Entrust recommends that you do not make any
+ playbooks take actions based on the content of this field. However it may be useful for debugging, logging, or auditing purposes.'
+ returned: success
+ type: dict
+
+'''
+
+from ansible_collections.community.crypto.plugins.module_utils.ecs.api import (
+ ecs_client_argument_spec,
+ ECSClient,
+ RestOperationException,
+ SessionConfigurationException,
+)
+
+import datetime
+import os
+import re
+import time
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.common.text.converters import to_native, to_bytes
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.io import (
+ write_file,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ load_certificate,
+)
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
+
+
+def validate_cert_expiry(cert_expiry):
+ search_string_partial = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])\Z')
+ search_string_full = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):'
+ r'([0-5][0-9]|60)(.[0-9]+)?(([Zz])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))\Z')
+ if search_string_partial.match(cert_expiry) or search_string_full.match(cert_expiry):
+ return True
+ return False
+
+
+def calculate_cert_days(expires_after):
+ cert_days = 0
+ if expires_after:
+ expires_after_datetime = datetime.datetime.strptime(expires_after, '%Y-%m-%dT%H:%M:%SZ')
+ cert_days = (expires_after_datetime - datetime.datetime.now()).days
+ return cert_days
+
+
+# Populate the value of body[dict_param_name] with the JSON equivalent of
+# module parameter of param_name if that parameter is present, otherwise leave field
+# out of resulting dict
+def convert_module_param_to_json_bool(module, dict_param_name, param_name):
+ body = {}
+ if module.params[param_name] is not None:
+ if module.params[param_name]:
+ body[dict_param_name] = 'true'
+ else:
+ body[dict_param_name] = 'false'
+ return body
+
+
+class EcsCertificate(object):
+ '''
+ Entrust Certificate Services certificate class.
+ '''
+
+ def __init__(self, module):
+ self.path = module.params['path']
+ self.full_chain_path = module.params['full_chain_path']
+ self.force = module.params['force']
+ self.backup = module.params['backup']
+ self.request_type = module.params['request_type']
+ self.csr = module.params['csr']
+
+ # All return values
+ self.changed = False
+ self.filename = None
+ self.tracking_id = None
+ self.cert_status = None
+ self.serial_number = None
+ self.cert_days = None
+ self.cert_details = None
+ self.backup_file = None
+ self.backup_full_chain_file = None
+
+ self.cert = None
+ self.ecs_client = None
+ if self.path and os.path.exists(self.path):
+ try:
+ self.cert = load_certificate(self.path, backend='cryptography')
+ except Exception as dummy:
+ self.cert = None
+ # Instantiate the ECS client and then try a no-op connection to verify credentials are valid
+ try:
+ self.ecs_client = ECSClient(
+ entrust_api_user=module.params['entrust_api_user'],
+ entrust_api_key=module.params['entrust_api_key'],
+ entrust_api_cert=module.params['entrust_api_client_cert_path'],
+ entrust_api_cert_key=module.params['entrust_api_client_cert_key_path'],
+ entrust_api_specification_path=module.params['entrust_api_specification_path']
+ )
+ except SessionConfigurationException as e:
+ module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e)))
+ try:
+ self.ecs_client.GetAppVersion()
+ except RestOperationException as e:
+ module.fail_json(msg='Please verify credential information. Received exception when testing ECS connection: {0}'.format(to_native(e.message)))
+
+ # Conversion of the fields that go into the 'tracking' parameter of the request object
+ def convert_tracking_params(self, module):
+ body = {}
+ tracking = {}
+ if module.params['requester_name']:
+ tracking['requesterName'] = module.params['requester_name']
+ if module.params['requester_email']:
+ tracking['requesterEmail'] = module.params['requester_email']
+ if module.params['requester_phone']:
+ tracking['requesterPhone'] = module.params['requester_phone']
+ if module.params['tracking_info']:
+ tracking['trackingInfo'] = module.params['tracking_info']
+ if module.params['custom_fields']:
+ # Omit custom fields from submitted dict if not present, instead of submitting them with value of 'null'
+ # The ECS API does technically accept null without error, but it complicates debugging user escalations and is unnecessary bandwidth.
+ custom_fields = {}
+ for k, v in module.params['custom_fields'].items():
+ if v is not None:
+ custom_fields[k] = v
+ tracking['customFields'] = custom_fields
+ if module.params['additional_emails']:
+ tracking['additionalEmails'] = module.params['additional_emails']
+ body['tracking'] = tracking
+ return body
+
+ def convert_cert_subject_params(self, module):
+ body = {}
+ if module.params['subject_alt_name']:
+ body['subjectAltName'] = module.params['subject_alt_name']
+ if module.params['org']:
+ body['org'] = module.params['org']
+ if module.params['ou']:
+ body['ou'] = module.params['ou']
+ return body
+
+ def convert_general_params(self, module):
+ body = {}
+ if module.params['eku']:
+ body['eku'] = module.params['eku']
+ if self.request_type == 'new':
+ body['certType'] = module.params['cert_type']
+ body['clientId'] = module.params['client_id']
+ body.update(convert_module_param_to_json_bool(module, 'ctLog', 'ct_log'))
+ body.update(convert_module_param_to_json_bool(module, 'endUserKeyStorageAgreement', 'end_user_key_storage_agreement'))
+ return body
+
+ def convert_expiry_params(self, module):
+ body = {}
+ if module.params['cert_lifetime']:
+ body['certLifetime'] = module.params['cert_lifetime']
+ elif module.params['cert_expiry']:
+ body['certExpiryDate'] = module.params['cert_expiry']
+ # If neither cerTLifetime or certExpiryDate was specified and the request type is new, default to 365 days
+ elif self.request_type != 'reissue':
+ gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
+ expiry = gmt_now + datetime.timedelta(days=365)
+ body['certExpiryDate'] = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
+ return body
+
+ def set_tracking_id_by_serial_number(self, module):
+ try:
+ # Use serial_number to identify if certificate is an Entrust Certificate
+ # with an associated tracking ID
+ serial_number = "{0:X}".format(self.cert.serial_number)
+ cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {})
+ if len(cert_results) == 1:
+ self.tracking_id = cert_results[0].get('trackingId')
+ except RestOperationException as dummy:
+ # If we fail to find a cert by serial number, that's fine, we just do not set self.tracking_id
+ return
+
+ def set_cert_details(self, module):
+ try:
+ self.cert_details = self.ecs_client.GetCertificate(trackingId=self.tracking_id)
+ self.cert_status = self.cert_details.get('status')
+ self.serial_number = self.cert_details.get('serialNumber')
+ self.cert_days = calculate_cert_days(self.cert_details.get('expiresAfter'))
+ except RestOperationException as e:
+ module.fail_json('Failed to get details of certificate with tracking_id="{0}", Error: '.format(self.tracking_id), to_native(e.message))
+
+ def check(self, module):
+ if self.cert:
+ # We will only treat a certificate as valid if it is found as a managed entrust cert.
+ # We will only set updated tracking ID based on certificate in "path" if it is managed by entrust.
+ self.set_tracking_id_by_serial_number(module)
+
+ if module.params['tracking_id'] and self.tracking_id and module.params['tracking_id'] != self.tracking_id:
+ module.warn('tracking_id parameter of "{0}" provided, but will be ignored. Valid certificate was present in path "{1}" with '
+ 'tracking_id of "{2}".'.format(module.params['tracking_id'], self.path, self.tracking_id))
+
+ # If we did not end up setting tracking_id based on existing cert, get from module params
+ if not self.tracking_id:
+ self.tracking_id = module.params['tracking_id']
+
+ if not self.tracking_id:
+ return False
+
+ self.set_cert_details(module)
+
+ if self.cert_status == 'EXPIRED' or self.cert_status == 'SUSPENDED' or self.cert_status == 'REVOKED':
+ return False
+ if self.cert_days < module.params['remaining_days']:
+ return False
+
+ return True
+
+ def request_cert(self, module):
+ if not self.check(module) or self.force:
+ body = {}
+
+ # Read the CSR contents
+ if self.csr and os.path.exists(self.csr):
+ with open(self.csr, 'r') as csr_file:
+ body['csr'] = csr_file.read()
+
+ # Check if the path is already a cert
+ # tracking_id may be set as a parameter or by get_cert_details if an entrust cert is in 'path'. If tracking ID is null
+ # We will be performing a reissue operation.
+ if self.request_type != 'new' and not self.tracking_id:
+ module.warn('No existing Entrust certificate found in path={0} and no tracking_id was provided, setting request_type to "new" for this task'
+ 'run. Future playbook runs that point to the pathination file in {1} will use request_type={2}'
+ .format(self.path, self.path, self.request_type))
+ self.request_type = 'new'
+ elif self.request_type == 'new' and self.tracking_id:
+ module.warn('Existing certificate being acted upon, but request_type is "new", so will be a new certificate issuance rather than a'
+ 'reissue or renew')
+ # Use cases where request type is new and no existing certificate, or where request type is reissue/renew and a valid
+ # existing certificate is found, do not need warnings.
+
+ body.update(self.convert_tracking_params(module))
+ body.update(self.convert_cert_subject_params(module))
+ body.update(self.convert_general_params(module))
+ body.update(self.convert_expiry_params(module))
+
+ if not module.check_mode:
+ try:
+ if self.request_type == 'validate_only':
+ body['validateOnly'] = 'true'
+ result = self.ecs_client.NewCertRequest(Body=body)
+ if self.request_type == 'new':
+ result = self.ecs_client.NewCertRequest(Body=body)
+ elif self.request_type == 'renew':
+ result = self.ecs_client.RenewCertRequest(trackingId=self.tracking_id, Body=body)
+ elif self.request_type == 'reissue':
+ result = self.ecs_client.ReissueCertRequest(trackingId=self.tracking_id, Body=body)
+ self.tracking_id = result.get('trackingId')
+ self.set_cert_details(module)
+ except RestOperationException as e:
+ module.fail_json(msg='Failed to request new certificate from Entrust (ECS) {0}'.format(e.message))
+
+ if self.request_type != 'validate_only':
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ write_file(module, to_bytes(self.cert_details.get('endEntityCert')))
+ if self.full_chain_path and self.cert_details.get('chainCerts'):
+ if self.backup:
+ self.backup_full_chain_file = module.backup_local(self.full_chain_path)
+ chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n'
+ write_file(module, to_bytes(chain_string), path=self.full_chain_path)
+ self.changed = True
+ # If there is no certificate present in path but a tracking ID was specified, save it to disk
+ elif not os.path.exists(self.path) and self.tracking_id:
+ if not module.check_mode:
+ write_file(module, to_bytes(self.cert_details.get('endEntityCert')))
+ if self.full_chain_path and self.cert_details.get('chainCerts'):
+ chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n'
+ write_file(module, to_bytes(chain_string), path=self.full_chain_path)
+ self.changed = True
+
+ def dump(self):
+ result = {
+ 'changed': self.changed,
+ 'filename': self.path,
+ 'tracking_id': self.tracking_id,
+ 'cert_status': self.cert_status,
+ 'serial_number': self.serial_number,
+ 'cert_days': self.cert_days,
+ 'cert_details': self.cert_details,
+ }
+ if self.backup_file:
+ result['backup_file'] = self.backup_file
+ result['backup_full_chain_file'] = self.backup_full_chain_file
+ return result
+
+
+def custom_fields_spec():
+ return dict(
+ text1=dict(type='str'),
+ text2=dict(type='str'),
+ text3=dict(type='str'),
+ text4=dict(type='str'),
+ text5=dict(type='str'),
+ text6=dict(type='str'),
+ text7=dict(type='str'),
+ text8=dict(type='str'),
+ text9=dict(type='str'),
+ text10=dict(type='str'),
+ text11=dict(type='str'),
+ text12=dict(type='str'),
+ text13=dict(type='str'),
+ text14=dict(type='str'),
+ text15=dict(type='str'),
+ number1=dict(type='float'),
+ number2=dict(type='float'),
+ number3=dict(type='float'),
+ number4=dict(type='float'),
+ number5=dict(type='float'),
+ date1=dict(type='str'),
+ date2=dict(type='str'),
+ date3=dict(type='str'),
+ date4=dict(type='str'),
+ date5=dict(type='str'),
+ email1=dict(type='str'),
+ email2=dict(type='str'),
+ email3=dict(type='str'),
+ email4=dict(type='str'),
+ email5=dict(type='str'),
+ dropdown1=dict(type='str'),
+ dropdown2=dict(type='str'),
+ dropdown3=dict(type='str'),
+ dropdown4=dict(type='str'),
+ dropdown5=dict(type='str'),
+ )
+
+
+def ecs_certificate_argument_spec():
+ return dict(
+ backup=dict(type='bool', default=False),
+ force=dict(type='bool', default=False),
+ path=dict(type='path', required=True),
+ full_chain_path=dict(type='path'),
+ tracking_id=dict(type='int'),
+ remaining_days=dict(type='int', default=30),
+ request_type=dict(type='str', default='new', choices=['new', 'renew', 'reissue', 'validate_only']),
+ cert_type=dict(type='str', choices=['STANDARD_SSL',
+ 'ADVANTAGE_SSL',
+ 'UC_SSL',
+ 'EV_SSL',
+ 'WILDCARD_SSL',
+ 'PRIVATE_SSL',
+ 'PD_SSL',
+ 'CODE_SIGNING',
+ 'EV_CODE_SIGNING',
+ 'CDS_INDIVIDUAL',
+ 'CDS_GROUP',
+ 'CDS_ENT_LITE',
+ 'CDS_ENT_PRO',
+ 'SMIME_ENT',
+ ]),
+ csr=dict(type='str'),
+ subject_alt_name=dict(type='list', elements='str'),
+ eku=dict(type='str', choices=['SERVER_AUTH', 'CLIENT_AUTH', 'SERVER_AND_CLIENT_AUTH']),
+ ct_log=dict(type='bool'),
+ client_id=dict(type='int', default=1),
+ org=dict(type='str'),
+ ou=dict(type='list', elements='str'),
+ end_user_key_storage_agreement=dict(type='bool'),
+ tracking_info=dict(type='str'),
+ requester_name=dict(type='str', required=True),
+ requester_email=dict(type='str', required=True),
+ requester_phone=dict(type='str', required=True),
+ additional_emails=dict(type='list', elements='str'),
+ custom_fields=dict(type='dict', default=None, options=custom_fields_spec()),
+ cert_expiry=dict(type='str'),
+ cert_lifetime=dict(type='str', choices=['P1Y', 'P2Y', 'P3Y']),
+ )
+
+
+def main():
+ ecs_argument_spec = ecs_client_argument_spec()
+ ecs_argument_spec.update(ecs_certificate_argument_spec())
+ module = AnsibleModule(
+ argument_spec=ecs_argument_spec,
+ required_if=(
+ ['request_type', 'new', ['cert_type']],
+ ['request_type', 'validate_only', ['cert_type']],
+ ['cert_type', 'CODE_SIGNING', ['end_user_key_storage_agreement']],
+ ['cert_type', 'EV_CODE_SIGNING', ['end_user_key_storage_agreement']],
+ ),
+ mutually_exclusive=(
+ ['cert_expiry', 'cert_lifetime'],
+ ),
+ supports_check_mode=True,
+ )
+
+ if not CRYPTOGRAPHY_FOUND or CRYPTOGRAPHY_VERSION < LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION):
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+
+ # If validate_only is used, pointing to an existing tracking_id is an invalid operation
+ if module.params['tracking_id']:
+ if module.params['request_type'] == 'new' or module.params['request_type'] == 'validate_only':
+ module.fail_json(msg='The tracking_id field is invalid when request_type="{0}".'.format(module.params['request_type']))
+
+ # A reissued request can not specify an expiration date or lifetime
+ if module.params['request_type'] == 'reissue':
+ if module.params['cert_expiry']:
+ module.fail_json(msg='The cert_expiry field is invalid when request_type="reissue".')
+ elif module.params['cert_lifetime']:
+ module.fail_json(msg='The cert_lifetime field is invalid when request_type="reissue".')
+ # Only a reissued request can omit the CSR
+ else:
+ module_params_csr = module.params['csr']
+ if module_params_csr is None:
+ module.fail_json(msg='The csr field is required when request_type={0}'.format(module.params['request_type']))
+ elif not os.path.exists(module_params_csr):
+ module.fail_json(msg='The csr field of {0} was not a valid path. csr is required when request_type={1}'.format(
+ module_params_csr, module.params['request_type']))
+
+ if module.params['ou'] and len(module.params['ou']) > 1:
+ module.fail_json(msg='Multiple "ou" values are not currently supported.')
+
+ if module.params['end_user_key_storage_agreement']:
+ if module.params['cert_type'] != 'CODE_SIGNING' and module.params['cert_type'] != 'EV_CODE_SIGNING':
+ module.fail_json(msg='Parameter "end_user_key_storage_agreement" is valid only for cert_types "CODE_SIGNING" and "EV_CODE_SIGNING"')
+
+ if module.params['org'] and module.params['client_id'] != 1 and module.params['cert_type'] != 'PD_SSL':
+ module.fail_json(msg='The "org" parameter is not supported when client_id parameter is set to a value other than 1, unless cert_type is "PD_SSL".')
+
+ if module.params['cert_expiry']:
+ if not validate_cert_expiry(module.params['cert_expiry']):
+ module.fail_json(msg='The "cert_expiry" parameter of "{0}" is not a valid date or date-time'.format(module.params['cert_expiry']))
+
+ certificate = EcsCertificate(module)
+ certificate.request_cert(module)
+ result = certificate.dump()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/ecs_domain.py b/ansible_collections/community/crypto/plugins/modules/ecs_domain.py
new file mode 100644
index 00000000..ec7ad98b
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/ecs_domain.py
@@ -0,0 +1,412 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright 2019 Entrust Datacard Corporation.
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: ecs_domain
+author:
+ - Chris Trufan (@ctrufan)
+version_added: '1.0.0'
+short_description: Request validation of a domain with the Entrust Certificate Services (ECS) API
+description:
+ - Request validation or re-validation of a domain with the Entrust Certificate Services (ECS) API.
+ - Requires credentials for the L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API.
+ - If the domain is already in the validation process, no new validation will be requested, but the validation data (if applicable) will be returned.
+ - If the domain is already in the validation process but the I(verification_method) specified is different than the current I(verification_method),
+ the I(verification_method) will be updated and validation data (if applicable) will be returned.
+ - If the domain is an active, validated domain, the return value of I(changed) will be false, unless C(domain_status=EXPIRED), in which case a re-validation
+ will be performed.
+ - If C(verification_method=dns), details about the required DNS entry will be specified in the return parameters I(dns_contents), I(dns_location), and
+ I(dns_resource_type).
+ - If C(verification_method=web_server), details about the required file details will be specified in the return parameters I(file_contents) and
+ I(file_location).
+ - If C(verification_method=email), the email address(es) that the validation email(s) were sent to will be in the return parameter I(emails). This is
+ purely informational. For domains requested using this module, this will always be a list of size 1.
+notes:
+ - There is a small delay (typically about 5 seconds, but can be as long as 60 seconds) before obtaining the random values when requesting a validation
+ while C(verification_method=dns) or C(verification_method=web_server). Be aware of that if doing many domain validation requests.
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - community.crypto.ecs_credential
+attributes:
+ check_mode:
+ support: none
+ diff_mode:
+ support: none
+options:
+ client_id:
+ description:
+ - The client ID to request the domain be associated with.
+ - If no client ID is specified, the domain will be added under the primary client with ID of 1.
+ type: int
+ default: 1
+ domain_name:
+ description:
+ - The domain name to be verified or reverified.
+ type: str
+ required: true
+ verification_method:
+ description:
+ - The verification method to be used to prove control of the domain.
+ - If C(verification_method=email) and the value I(verification_email) is specified, that value is used for the email validation. If
+ I(verification_email) is not provided, the first value present in WHOIS data will be used. An email will be sent to the address in
+ I(verification_email) with instructions on how to verify control of the domain.
+ - If C(verification_method=dns), the value I(dns_contents) must be stored in location I(dns_location), with a DNS record type of
+ I(verification_dns_record_type). To prove domain ownership, update your DNS records so the text string returned by I(dns_contents) is available at
+ I(dns_location).
+ - If C(verification_method=web_server), the contents of return value I(file_contents) must be made available on a web server accessible at location
+ I(file_location).
+ - If C(verification_method=manual), the domain will be validated with a manual process. This is not recommended.
+ type: str
+ choices: [ 'dns', 'email', 'manual', 'web_server']
+ required: true
+ verification_email:
+ description:
+ - Email address to be used to verify domain ownership.
+ - 'Email address must be either an email address present in the WHOIS data for I(domain_name), or one of the following constructed emails:
+ admin@I(domain_name), administrator@I(domain_name), webmaster@I(domain_name), hostmaster@I(domain_name), postmaster@I(domain_name).'
+ - 'Note that if I(domain_name) includes subdomains, the top level domain should be used. For example, if requesting validation of
+ example1.ansible.com, or test.example2.ansible.com, and you want to use the "admin" preconstructed name, the email address should be
+ admin@ansible.com.'
+ - If using the email values from the WHOIS data for the domain or its top level namespace, they must be exact matches.
+ - If C(verification_method=email) but I(verification_email) is not provided, the first email address found in WHOIS data for the domain will be
+ used.
+ - To verify domain ownership, domain owner must follow the instructions in the email they receive.
+ - Only allowed if C(verification_method=email)
+ type: str
+seealso:
+ - module: community.crypto.x509_certificate
+ description: Can be used to request certificates from ECS, with C(provider=entrust).
+ - module: community.crypto.ecs_certificate
+ description: Can be used to request a Certificate from ECS using a verified domain.
+'''
+
+EXAMPLES = r'''
+- name: Request domain validation using email validation for client ID of 2.
+ community.crypto.ecs_domain:
+ domain_name: ansible.com
+ client_id: 2
+ verification_method: email
+ verification_email: admin@ansible.com
+ entrust_api_user: apiusername
+ entrust_api_key: a^lv*32!cd9LnT
+ entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
+ entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
+
+- name: Request domain validation using DNS. If domain is already valid,
+ request revalidation if expires within 90 days
+ community.crypto.ecs_domain:
+ domain_name: ansible.com
+ verification_method: dns
+ entrust_api_user: apiusername
+ entrust_api_key: a^lv*32!cd9LnT
+ entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
+ entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
+
+- name: Request domain validation using web server validation, and revalidate
+ if fewer than 60 days remaining of EV eligibility.
+ community.crypto.ecs_domain:
+ domain_name: ansible.com
+ verification_method: web_server
+ entrust_api_user: apiusername
+ entrust_api_key: a^lv*32!cd9LnT
+ entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
+ entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
+
+- name: Request domain validation using manual validation.
+ community.crypto.ecs_domain:
+ domain_name: ansible.com
+ verification_method: manual
+ entrust_api_user: apiusername
+ entrust_api_key: a^lv*32!cd9LnT
+ entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
+ entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-client.key
+'''
+
+RETURN = '''
+domain_status:
+ description: Status of the current domain. Will be one of C(APPROVED), C(DECLINED), C(CANCELLED), C(INITIAL_VERIFICATION), C(DECLINED), C(CANCELLED),
+ C(RE_VERIFICATION), C(EXPIRED), C(EXPIRING)
+ returned: changed or success
+ type: str
+ sample: APPROVED
+verification_method:
+ description: Verification method used to request the domain validation. If C(changed) will be the same as I(verification_method) input parameter.
+ returned: changed or success
+ type: str
+ sample: dns
+file_location:
+ description: The location that ECS will be expecting to be able to find the file for domain verification, containing the contents of I(file_contents).
+ returned: I(verification_method) is C(web_server)
+ type: str
+ sample: http://ansible.com/.well-known/pki-validation/abcd.txt
+file_contents:
+ description: The contents of the file that ECS will be expecting to find at C(file_location).
+ returned: I(verification_method) is C(web_server)
+ type: str
+ sample: AB23CD41432522FF2526920393982FAB
+emails:
+ description:
+ - The list of emails used to request validation of this domain.
+ - Domains requested using this module will only have a list of size 1.
+ returned: I(verification_method) is C(email)
+ type: list
+ sample: [ admin@ansible.com, administrator@ansible.com ]
+dns_location:
+ description: The location that ECS will be expecting to be able to find the DNS entry for domain verification, containing the contents of I(dns_contents).
+ returned: changed and if I(verification_method) is C(dns)
+ type: str
+ sample: _pki-validation.ansible.com
+dns_contents:
+ description: The value that ECS will be expecting to find in the DNS record located at I(dns_location).
+ returned: changed and if I(verification_method) is C(dns)
+ type: str
+ sample: AB23CD41432522FF2526920393982FAB
+dns_resource_type:
+ description: The type of resource record that ECS will be expecting for the DNS record located at I(dns_location).
+ returned: changed and if I(verification_method) is C(dns)
+ type: str
+ sample: TXT
+client_id:
+ description: Client ID that the domain belongs to. If the input value I(client_id) is specified, this will always be the same as I(client_id)
+ returned: changed or success
+ type: int
+ sample: 1
+ov_eligible:
+ description: Whether the domain is eligible for submission of "OV" certificates. Will never be C(false) if I(ov_eligible) is C(true)
+ returned: success and I(domain_status) is C(APPROVED), C(RE_VERIFICATION), C(EXPIRING), or C(EXPIRED).
+ type: bool
+ sample: true
+ov_days_remaining:
+ description: The number of days the domain remains eligible for submission of "OV" certificates. Will never be less than the value of I(ev_days_remaining)
+ returned: success and I(ov_eligible) is C(true) and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING).
+ type: int
+ sample: 129
+ev_eligible:
+ description: Whether the domain is eligible for submission of "EV" certificates. Will never be C(true) if I(ov_eligible) is C(false)
+ returned: success and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING), or C(EXPIRED).
+ type: bool
+ sample: true
+ev_days_remaining:
+ description: The number of days the domain remains eligible for submission of "EV" certificates. Will never be greater than the value of
+ I(ov_days_remaining)
+ returned: success and I(ev_eligible) is C(true) and I(domain_status) is C(APPROVED), C(RE_VERIFICATION) or C(EXPIRING).
+ type: int
+ sample: 94
+
+'''
+
+import datetime
+import time
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.ecs.api import (
+ ecs_client_argument_spec,
+ ECSClient,
+ RestOperationException,
+ SessionConfigurationException,
+)
+
+
+def calculate_days_remaining(expiry_date):
+ days_remaining = None
+ if expiry_date:
+ expiry_datetime = datetime.datetime.strptime(expiry_date, '%Y-%m-%dT%H:%M:%SZ')
+ days_remaining = (expiry_datetime - datetime.datetime.now()).days
+ return days_remaining
+
+
+class EcsDomain(object):
+ '''
+ Entrust Certificate Services domain class.
+ '''
+
+ def __init__(self, module):
+ self.changed = False
+ self.domain_status = None
+ self.verification_method = None
+ self.file_location = None
+ self.file_contents = None
+ self.dns_location = None
+ self.dns_contents = None
+ self.dns_resource_type = None
+ self.emails = None
+ self.ov_eligible = None
+ self.ov_days_remaining = None
+ self.ev_eligble = None
+ self.ev_days_remaining = None
+ # Note that verification_method is the 'current' verification
+ # method of the domain, we'll use module.params when requesting a new
+ # one, in case the verification method has changed.
+ self.verification_method = None
+
+ self.ecs_client = None
+ # Instantiate the ECS client and then try a no-op connection to verify credentials are valid
+ try:
+ self.ecs_client = ECSClient(
+ entrust_api_user=module.params['entrust_api_user'],
+ entrust_api_key=module.params['entrust_api_key'],
+ entrust_api_cert=module.params['entrust_api_client_cert_path'],
+ entrust_api_cert_key=module.params['entrust_api_client_cert_key_path'],
+ entrust_api_specification_path=module.params['entrust_api_specification_path']
+ )
+ except SessionConfigurationException as e:
+ module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e)))
+ try:
+ self.ecs_client.GetAppVersion()
+ except RestOperationException as e:
+ module.fail_json(msg='Please verify credential information. Received exception when testing ECS connection: {0}'.format(to_native(e.message)))
+
+ def set_domain_details(self, domain_details):
+ if domain_details.get('verificationMethod'):
+ self.verification_method = domain_details['verificationMethod'].lower()
+ self.domain_status = domain_details['verificationStatus']
+ self.ov_eligible = domain_details.get('ovEligible')
+ self.ov_days_remaining = calculate_days_remaining(domain_details.get('ovExpiry'))
+ self.ev_eligible = domain_details.get('evEligible')
+ self.ev_days_remaining = calculate_days_remaining(domain_details.get('evExpiry'))
+ self.client_id = domain_details['clientId']
+
+ if self.verification_method == 'dns' and domain_details.get('dnsMethod'):
+ self.dns_location = domain_details['dnsMethod']['recordDomain']
+ self.dns_resource_type = domain_details['dnsMethod']['recordType']
+ self.dns_contents = domain_details['dnsMethod']['recordValue']
+ elif self.verification_method == 'web_server' and domain_details.get('webServerMethod'):
+ self.file_location = domain_details['webServerMethod']['fileLocation']
+ self.file_contents = domain_details['webServerMethod']['fileContents']
+ elif self.verification_method == 'email' and domain_details.get('emailMethod'):
+ self.emails = domain_details['emailMethod']
+
+ def check(self, module):
+ try:
+ domain_details = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name'])
+ self.set_domain_details(domain_details)
+ if self.domain_status != 'APPROVED' and self.domain_status != 'INITIAL_VERIFICATION' and self.domain_status != 'RE_VERIFICATION':
+ return False
+
+ # If domain verification is in process, we want to return the random values and treat it as a valid.
+ if self.domain_status == 'INITIAL_VERIFICATION' or self.domain_status == 'RE_VERIFICATION':
+ # Unless the verification method has changed, in which case we need to do a reverify request.
+ if self.verification_method != module.params['verification_method']:
+ return False
+
+ if self.domain_status == 'EXPIRING':
+ return False
+
+ return True
+ except RestOperationException as dummy:
+ return False
+
+ def request_domain(self, module):
+ if not self.check(module):
+ body = {}
+
+ body['verificationMethod'] = module.params['verification_method'].upper()
+ if module.params['verification_method'] == 'email':
+ emailMethod = {}
+ if module.params['verification_email']:
+ emailMethod['emailSource'] = 'SPECIFIED'
+ emailMethod['email'] = module.params['verification_email']
+ else:
+ emailMethod['emailSource'] = 'INCLUDE_WHOIS'
+ body['emailMethod'] = emailMethod
+ # Only populate domain name in body if it is not an existing domain
+ if not self.domain_status:
+ body['domainName'] = module.params['domain_name']
+ try:
+ if not self.domain_status:
+ self.ecs_client.AddDomain(clientId=module.params['client_id'], Body=body)
+ else:
+ self.ecs_client.ReverifyDomain(clientId=module.params['client_id'], domain=module.params['domain_name'], Body=body)
+
+ time.sleep(5)
+ result = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name'])
+
+ # It takes a bit of time before the random values are available
+ if module.params['verification_method'] == 'dns' or module.params['verification_method'] == 'web_server':
+ for i in range(4):
+ # Check both that random values are now available, and that they're different than were populated by previous 'check'
+ if module.params['verification_method'] == 'dns':
+ if result.get('dnsMethod') and result['dnsMethod']['recordValue'] != self.dns_contents:
+ break
+ elif module.params['verification_method'] == 'web_server':
+ if result.get('webServerMethod') and result['webServerMethod']['fileContents'] != self.file_contents:
+ break
+ time.sleep(10)
+ result = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name'])
+ self.changed = True
+ self.set_domain_details(result)
+ except RestOperationException as e:
+ module.fail_json(msg='Failed to request domain validation from Entrust (ECS) {0}'.format(e.message))
+
+ def dump(self):
+ result = {
+ 'changed': self.changed,
+ 'client_id': self.client_id,
+ 'domain_status': self.domain_status,
+ }
+
+ if self.verification_method:
+ result['verification_method'] = self.verification_method
+ if self.ov_eligible is not None:
+ result['ov_eligible'] = self.ov_eligible
+ if self.ov_days_remaining:
+ result['ov_days_remaining'] = self.ov_days_remaining
+ if self.ev_eligible is not None:
+ result['ev_eligible'] = self.ev_eligible
+ if self.ev_days_remaining:
+ result['ev_days_remaining'] = self.ev_days_remaining
+ if self.emails:
+ result['emails'] = self.emails
+
+ if self.verification_method == 'dns':
+ result['dns_location'] = self.dns_location
+ result['dns_contents'] = self.dns_contents
+ result['dns_resource_type'] = self.dns_resource_type
+ elif self.verification_method == 'web_server':
+ result['file_location'] = self.file_location
+ result['file_contents'] = self.file_contents
+ elif self.verification_method == 'email':
+ result['emails'] = self.emails
+
+ return result
+
+
+def ecs_domain_argument_spec():
+ return dict(
+ client_id=dict(type='int', default=1),
+ domain_name=dict(type='str', required=True),
+ verification_method=dict(type='str', required=True, choices=['dns', 'email', 'manual', 'web_server']),
+ verification_email=dict(type='str'),
+ )
+
+
+def main():
+ ecs_argument_spec = ecs_client_argument_spec()
+ ecs_argument_spec.update(ecs_domain_argument_spec())
+ module = AnsibleModule(
+ argument_spec=ecs_argument_spec,
+ supports_check_mode=False,
+ )
+
+ if module.params['verification_email'] and module.params['verification_method'] != 'email':
+ module.fail_json(msg='The verification_email field is invalid when verification_method="{0}".'.format(module.params['verification_method']))
+
+ domain = EcsDomain(module)
+ domain.request_domain(module)
+ result = domain.dump()
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/get_certificate.py b/ansible_collections/community/crypto/plugins/modules/get_certificate.py
new file mode 100644
index 00000000..066930b0
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/get_certificate.py
@@ -0,0 +1,397 @@
+#!/usr/bin/python
+# coding: utf-8 -*-
+
+# Copyright (c) Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: get_certificate
+author: "John Westcott IV (@john-westcott-iv)"
+short_description: Get a certificate from a host:port
+description:
+ - Makes a secure connection and returns information about the presented certificate
+ - The module uses the cryptography Python library.
+ - Support SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) only with python >= 2.7.
+extends_documentation_fragment:
+ - community.crypto.attributes
+attributes:
+ check_mode:
+ support: none
+ details:
+ - This action does not modify state.
+ diff_mode:
+ support: N/A
+ details:
+ - This action does not modify state.
+options:
+ host:
+ description:
+ - The host to get the cert for (IP is fine)
+ type: str
+ required: true
+ ca_cert:
+ description:
+ - A PEM file containing one or more root certificates; if present, the cert will be validated against these root certs.
+ - Note that this only validates the certificate is signed by the chain; not that the cert is valid for the host presenting it.
+ type: path
+ port:
+ description:
+ - The port to connect to
+ type: int
+ required: true
+ server_name:
+ description:
+ - Server name used for SNI (L(Server Name Indication,https://en.wikipedia.org/wiki/Server_Name_Indication)) when hostname
+ is an IP or is different from server name.
+ type: str
+ version_added: 1.4.0
+ proxy_host:
+ description:
+ - Proxy host used when get a certificate.
+ type: str
+ proxy_port:
+ description:
+ - Proxy port used when get a certificate.
+ type: int
+ default: 8080
+ starttls:
+ description:
+ - Requests a secure connection for protocols which require clients to initiate encryption.
+ - Only available for C(mysql) currently.
+ type: str
+ choices:
+ - mysql
+ version_added: 1.9.0
+ timeout:
+ description:
+ - The timeout in seconds
+ type: int
+ default: 10
+ select_crypto_backend:
+ description:
+ - Determines which crypto backend to use.
+ - The default choice is C(auto), which tries to use C(cryptography) if available.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography ]
+ ciphers:
+ description:
+ - SSL/TLS Ciphers to use for the request.
+ - 'When a list is provided, all ciphers are joined in order with C(:).'
+ - See the L(OpenSSL Cipher List Format,https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html#CIPHER-LIST-FORMAT)
+ for more details.
+ - The available ciphers is dependent on the Python and OpenSSL/LibreSSL versions.
+ type: list
+ elements: str
+ version_added: 2.11.0
+
+notes:
+ - When using ca_cert on OS X it has been reported that in some conditions the validate will always succeed.
+
+requirements:
+ - "python >= 2.7 when using C(proxy_host)"
+ - "cryptography >= 1.6"
+'''
+
+RETURN = '''
+cert:
+ description: The certificate retrieved from the port
+ returned: success
+ type: str
+expired:
+ description: Boolean indicating if the cert is expired
+ returned: success
+ type: bool
+extensions:
+ description: Extensions applied to the cert
+ returned: success
+ type: list
+ elements: dict
+ contains:
+ critical:
+ returned: success
+ type: bool
+ description: Whether the extension is critical.
+ asn1_data:
+ returned: success
+ type: str
+ description:
+ - The Base64 encoded ASN.1 content of the extension.
+ - B(Note) that depending on the C(cryptography) version used, it is
+ not possible to extract the ASN.1 content of the extension, but only
+ to provide the re-encoded content of the extension in case it was
+ parsed by C(cryptography). This should usually result in exactly the
+ same value, except if the original extension value was malformed.
+ name:
+ returned: success
+ type: str
+ description: The extension's name.
+issuer:
+ description: Information about the issuer of the cert
+ returned: success
+ type: dict
+not_after:
+ description: Expiration date of the cert
+ returned: success
+ type: str
+not_before:
+ description: Issue date of the cert
+ returned: success
+ type: str
+serial_number:
+ description: The serial number of the cert
+ returned: success
+ type: str
+signature_algorithm:
+ description: The algorithm used to sign the cert
+ returned: success
+ type: str
+subject:
+ description: Information about the subject of the cert (OU, CN, etc)
+ returned: success
+ type: dict
+version:
+ description: The version number of the certificate
+ returned: success
+ type: str
+'''
+
+EXAMPLES = '''
+- name: Get the cert from an RDP port
+ community.crypto.get_certificate:
+ host: "1.2.3.4"
+ port: 3389
+ delegate_to: localhost
+ run_once: true
+ register: cert
+
+- name: Get a cert from an https port
+ community.crypto.get_certificate:
+ host: "www.google.com"
+ port: 443
+ delegate_to: localhost
+ run_once: true
+ register: cert
+
+- name: How many days until cert expires
+ debug:
+ msg: "cert expires in: {{ expire_days }} days."
+ vars:
+ expire_days: "{{ (( cert.not_after | to_datetime('%Y%m%d%H%M%SZ')) - (ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ')) ).days }}"
+'''
+
+import atexit
+import base64
+import datetime
+import traceback
+
+from os.path import isfile
+from socket import create_connection, setdefaulttimeout, socket
+from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_REQUIRED
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.common.text.converters import to_bytes
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
+ cryptography_oid_to_name,
+ cryptography_get_extensions_from_cert,
+)
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
+
+CREATE_DEFAULT_CONTEXT_IMP_ERR = None
+try:
+ from ssl import create_default_context
+except ImportError:
+ CREATE_DEFAULT_CONTEXT_IMP_ERR = traceback.format_exc()
+ HAS_CREATE_DEFAULT_CONTEXT = False
+else:
+ HAS_CREATE_DEFAULT_CONTEXT = True
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ import cryptography.exceptions
+ import cryptography.x509
+ from cryptography.hazmat.backends import default_backend as cryptography_backend
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+
+def send_starttls_packet(sock, server_type):
+ if server_type == 'mysql':
+ ssl_request_packet = (
+ b'\x20\x00\x00\x01\x85\xae\x7f\x00' +
+ b'\x00\x00\x00\x01\x21\x00\x00\x00' +
+ b'\x00\x00\x00\x00\x00\x00\x00\x00' +
+ b'\x00\x00\x00\x00\x00\x00\x00\x00' +
+ b'\x00\x00\x00\x00'
+ )
+
+ sock.recv(8192) # discard initial handshake from server for this naive implementation
+ sock.send(ssl_request_packet)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ ca_cert=dict(type='path'),
+ host=dict(type='str', required=True),
+ port=dict(type='int', required=True),
+ proxy_host=dict(type='str'),
+ proxy_port=dict(type='int', default=8080),
+ server_name=dict(type='str'),
+ timeout=dict(type='int', default=10),
+ select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
+ starttls=dict(type='str', choices=['mysql']),
+ ciphers=dict(type='list', elements='str'),
+ ),
+ )
+
+ ca_cert = module.params.get('ca_cert')
+ host = module.params.get('host')
+ port = module.params.get('port')
+ proxy_host = module.params.get('proxy_host')
+ proxy_port = module.params.get('proxy_port')
+ timeout = module.params.get('timeout')
+ server_name = module.params.get('server_name')
+ start_tls_server_type = module.params.get('starttls')
+ ciphers = module.params.get('ciphers')
+
+ backend = module.params.get('select_crypto_backend')
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
+
+ # Try cryptography
+ if can_use_cryptography:
+ backend = 'cryptography'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect the required Python library "
+ "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
+
+ if backend == 'cryptography':
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+
+ result = dict(
+ changed=False,
+ )
+
+ if timeout:
+ setdefaulttimeout(timeout)
+
+ if ca_cert:
+ if not isfile(ca_cert):
+ module.fail_json(msg="ca_cert file does not exist")
+
+ if not HAS_CREATE_DEFAULT_CONTEXT:
+ # Python < 2.7.9
+ if proxy_host:
+ module.fail_json(msg='To use proxy_host, you must run the get_certificate module with Python 2.7 or newer.',
+ exception=CREATE_DEFAULT_CONTEXT_IMP_ERR)
+ if ciphers is not None:
+ module.fail_json(msg='To use ciphers, you must run the get_certificate module with Python 2.7 or newer.',
+ exception=CREATE_DEFAULT_CONTEXT_IMP_ERR)
+ try:
+ # Note: get_server_certificate does not support SNI!
+ cert = get_server_certificate((host, port), ca_certs=ca_cert)
+ except Exception as e:
+ module.fail_json(msg="Failed to get cert from {0}:{1}, error: {2}".format(host, port, e))
+ else:
+ # Python >= 2.7.9
+ try:
+ if proxy_host:
+ connect = "CONNECT %s:%s HTTP/1.0\r\n\r\n" % (host, port)
+ sock = socket()
+ atexit.register(sock.close)
+ sock.connect((proxy_host, proxy_port))
+ sock.send(connect.encode())
+ sock.recv(8192)
+ else:
+ sock = create_connection((host, port))
+ atexit.register(sock.close)
+
+ if ca_cert:
+ ctx = create_default_context(cafile=ca_cert)
+ ctx.check_hostname = False
+ ctx.verify_mode = CERT_REQUIRED
+ else:
+ ctx = create_default_context()
+ ctx.check_hostname = False
+ ctx.verify_mode = CERT_NONE
+
+ if start_tls_server_type is not None:
+ send_starttls_packet(sock, start_tls_server_type)
+
+ if ciphers is not None:
+ ciphers_joined = ":".join(ciphers)
+ ctx.set_ciphers(ciphers_joined)
+
+ cert = ctx.wrap_socket(sock, server_hostname=server_name or host).getpeercert(True)
+ cert = DER_cert_to_PEM_cert(cert)
+ except Exception as e:
+ if proxy_host:
+ module.fail_json(msg="Failed to get cert via proxy {0}:{1} from {2}:{3}, error: {4}".format(
+ proxy_host, proxy_port, host, port, e))
+ else:
+ module.fail_json(msg="Failed to get cert from {0}:{1}, error: {2}".format(host, port, e))
+
+ result['cert'] = cert
+
+ if backend == 'cryptography':
+ x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography_backend())
+ result['subject'] = {}
+ for attribute in x509.subject:
+ result['subject'][cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value
+
+ result['expired'] = x509.not_valid_after < datetime.datetime.utcnow()
+
+ result['extensions'] = []
+ for dotted_number, entry in cryptography_get_extensions_from_cert(x509).items():
+ oid = cryptography.x509.oid.ObjectIdentifier(dotted_number)
+ result['extensions'].append({
+ 'critical': entry['critical'],
+ 'asn1_data': base64.b64decode(entry['value']),
+ 'name': cryptography_oid_to_name(oid, short=True),
+ })
+
+ result['issuer'] = {}
+ for attribute in x509.issuer:
+ result['issuer'][cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value
+
+ result['not_after'] = x509.not_valid_after.strftime('%Y%m%d%H%M%SZ')
+ result['not_before'] = x509.not_valid_before.strftime('%Y%m%d%H%M%SZ')
+
+ result['serial_number'] = x509.serial_number
+ result['signature_algorithm'] = cryptography_oid_to_name(x509.signature_algorithm_oid)
+
+ # We need the -1 offset to get the same values as pyOpenSSL
+ if x509.version == cryptography.x509.Version.v1:
+ result['version'] = 1 - 1
+ elif x509.version == cryptography.x509.Version.v3:
+ result['version'] = 3 - 1
+ else:
+ result['version'] = "unknown"
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/luks_device.py b/ansible_collections/community/crypto/plugins/modules/luks_device.py
new file mode 100644
index 00000000..d8b70e74
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/luks_device.py
@@ -0,0 +1,1031 @@
+#!/usr/bin/python
+# Copyright (c) 2017 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: luks_device
+
+short_description: Manage encrypted (LUKS) devices
+
+description:
+ - "Module manages L(LUKS,https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup)
+ on given device. Supports creating, destroying, opening and closing of
+ LUKS container and adding or removing new keys and passphrases."
+
+extends_documentation_fragment:
+ - community.crypto.attributes
+
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+
+options:
+ device:
+ description:
+ - "Device to work with (for example C(/dev/sda1)). Needed in most cases.
+ Can be omitted only when I(state=closed) together with I(name)
+ is provided."
+ type: str
+ state:
+ description:
+ - "Desired state of the LUKS container. Based on its value creates,
+ destroys, opens or closes the LUKS container on a given device."
+ - "I(present) will create LUKS container unless already present.
+ Requires I(device) and either I(keyfile) or I(passphrase) options
+ to be provided."
+ - "I(absent) will remove existing LUKS container if it exists.
+ Requires I(device) or I(name) to be specified."
+ - "I(opened) will unlock the LUKS container. If it does not exist
+ it will be created first.
+ Requires I(device) and either I(keyfile) or I(passphrase)
+ to be specified. Use the I(name) option to set the name of
+ the opened container. Otherwise the name will be
+ generated automatically and returned as a part of the
+ result."
+ - "I(closed) will lock the LUKS container. However if the container
+ does not exist it will be created.
+ Requires I(device) and either I(keyfile) or I(passphrase)
+ options to be provided. If container does already exist
+ I(device) or I(name) will suffice."
+ type: str
+ default: present
+ choices: [present, absent, opened, closed]
+ name:
+ description:
+ - "Sets container name when I(state=opened). Can be used
+ instead of I(device) when closing the existing container
+ (that is, when I(state=closed))."
+ type: str
+ keyfile:
+ description:
+ - "Used to unlock the container. Either a I(keyfile) or a
+ I(passphrase) is needed for most of the operations. Parameter
+ value is the path to the keyfile with the passphrase."
+ - "BEWARE that working with keyfiles in plaintext is dangerous.
+ Make sure that they are protected."
+ type: path
+ passphrase:
+ description:
+ - "Used to unlock the container. Either a I(passphrase) or a
+ I(keyfile) is needed for most of the operations. Parameter
+ value is a string with the passphrase."
+ type: str
+ version_added: '1.0.0'
+ keysize:
+ description:
+ - "Sets the key size only if LUKS container does not exist."
+ type: int
+ version_added: '1.0.0'
+ new_keyfile:
+ description:
+ - "Adds additional key to given container on I(device).
+ Needs I(keyfile) or I(passphrase) option for authorization.
+ LUKS container supports up to 8 keyslots. Parameter value
+ is the path to the keyfile with the passphrase."
+ - "NOTE that adding additional keys is idempotent only since
+ community.crypto 1.4.0. For older versions, a new keyslot
+ will be used even if another keyslot already exists for this
+ keyfile."
+ - "BEWARE that working with keyfiles in plaintext is dangerous.
+ Make sure that they are protected."
+ type: path
+ new_passphrase:
+ description:
+ - "Adds additional passphrase to given container on I(device).
+ Needs I(keyfile) or I(passphrase) option for authorization. LUKS
+ container supports up to 8 keyslots. Parameter value is a string
+ with the new passphrase."
+ - "NOTE that adding additional passphrase is idempotent only since
+ community.crypto 1.4.0. For older versions, a new keyslot will
+ be used even if another keyslot already exists for this passphrase."
+ type: str
+ version_added: '1.0.0'
+ remove_keyfile:
+ description:
+ - "Removes given key from the container on I(device). Does not
+ remove the keyfile from filesystem.
+ Parameter value is the path to the keyfile with the passphrase."
+ - "NOTE that removing keys is idempotent only since
+ community.crypto 1.4.0. For older versions, trying to remove
+ a key which no longer exists results in an error."
+ - "NOTE that to remove the last key from a LUKS container, the
+ I(force_remove_last_key) option must be set to C(true)."
+ - "BEWARE that working with keyfiles in plaintext is dangerous.
+ Make sure that they are protected."
+ type: path
+ remove_passphrase:
+ description:
+ - "Removes given passphrase from the container on I(device).
+ Parameter value is a string with the passphrase to remove."
+ - "NOTE that removing passphrases is idempotent only since
+ community.crypto 1.4.0. For older versions, trying to remove
+ a passphrase which no longer exists results in an error."
+ - "NOTE that to remove the last keyslot from a LUKS
+ container, the I(force_remove_last_key) option must be set
+ to C(true)."
+ type: str
+ version_added: '1.0.0'
+ force_remove_last_key:
+ description:
+ - "If set to C(true), allows removing the last key from a container."
+ - "BEWARE that when the last key has been removed from a container,
+ the container can no longer be opened!"
+ type: bool
+ default: false
+ label:
+ description:
+ - "This option allow the user to create a LUKS2 format container
+ with label support, respectively to identify the container by
+ label on later usages."
+ - "Will only be used on container creation, or when I(device) is
+ not specified."
+ - "This cannot be specified if I(type) is set to C(luks1)."
+ type: str
+ version_added: '1.0.0'
+ uuid:
+ description:
+ - "With this option user can identify the LUKS container by UUID."
+ - "Will only be used when I(device) and I(label) are not specified."
+ type: str
+ version_added: '1.0.0'
+ type:
+ description:
+ - "This option allow the user explicit define the format of LUKS
+ container that wants to work with. Options are C(luks1) or C(luks2)"
+ type: str
+ choices: [luks1, luks2]
+ version_added: '1.0.0'
+ cipher:
+ description:
+ - "This option allows the user to define the cipher specification
+ string for the LUKS container."
+ - "Will only be used on container creation."
+ - "For pre-2.6.10 kernels, use C(aes-plain) as they do not understand
+ the new cipher spec strings. To use ESSIV, use C(aes-cbc-essiv:sha256)."
+ type: str
+ version_added: '1.1.0'
+ hash:
+ description:
+ - "This option allows the user to specify the hash function used in LUKS
+ key setup scheme and volume key digest."
+ - "Will only be used on container creation."
+ type: str
+ version_added: '1.1.0'
+ pbkdf:
+ description:
+ - This option allows the user to configure the Password-Based Key Derivation
+ Function (PBKDF) used.
+ - Will only be used on container creation, and when adding keys to an existing
+ container.
+ type: dict
+ version_added: '1.4.0'
+ suboptions:
+ iteration_time:
+ description:
+ - Specify the iteration time used for the PBKDF.
+ - Note that this is in B(seconds), not in milliseconds as on the
+ command line.
+ - Mutually exclusive with I(iteration_count).
+ type: float
+ iteration_count:
+ description:
+ - Specify the iteration count used for the PBKDF.
+ - Mutually exclusive with I(iteration_time).
+ type: int
+ algorithm:
+ description:
+ - The algorithm to use.
+ - Only available for the LUKS 2 format.
+ choices:
+ - argon2i
+ - argon2id
+ - pbkdf2
+ type: str
+ memory:
+ description:
+ - The memory cost limit in kilobytes for the PBKDF.
+ - This is not used for PBKDF2, but only for the Argon PBKDFs.
+ type: int
+ parallel:
+ description:
+ - The parallel cost for the PBKDF. This is the number of threads that
+ run in parallel.
+ - This is not used for PBKDF2, but only for the Argon PBKDFs.
+ type: int
+ sector_size:
+ description:
+ - "This option allows the user to specify the sector size (in bytes) used for LUKS2 containers."
+ - "Will only be used on container creation."
+ type: int
+ version_added: '1.5.0'
+ perf_same_cpu_crypt:
+ description:
+ - "Allows the user to perform encryption using the same CPU that IO was submitted on."
+ - "The default is to use an unbound workqueue so that encryption work is automatically balanced between available CPUs."
+ - "Will only be used when opening containers."
+ type: bool
+ default: false
+ version_added: '2.3.0'
+ perf_submit_from_crypt_cpus:
+ description:
+ - "Allows the user to disable offloading writes to a separate thread after encryption."
+ - "There are some situations where offloading block write IO operations from the encryption threads
+ to a single thread degrades performance significantly."
+ - "The default is to offload block write IO operations to the same thread."
+ - "Will only be used when opening containers."
+ type: bool
+ default: false
+ version_added: '2.3.0'
+ perf_no_read_workqueue:
+ description:
+ - "Allows the user to bypass dm-crypt internal workqueue and process read requests synchronously."
+ - "Will only be used when opening containers."
+ type: bool
+ default: false
+ version_added: '2.3.0'
+ perf_no_write_workqueue:
+ description:
+ - "Allows the user to bypass dm-crypt internal workqueue and process write requests synchronously."
+ - "Will only be used when opening containers."
+ type: bool
+ default: false
+ version_added: '2.3.0'
+ persistent:
+ description:
+ - "Allows the user to store options into container's metadata persistently and automatically use them next time.
+ Only I(perf_same_cpu_crypt), I(perf_submit_from_crypt_cpus), I(perf_no_read_workqueue), and I(perf_no_write_workqueue)
+ can be stored persistently."
+ - "Will only work with LUKS2 containers."
+ - "Will only be used when opening containers."
+ type: bool
+ default: false
+ version_added: '2.3.0'
+
+requirements:
+ - "cryptsetup"
+ - "wipefs (when I(state) is C(absent))"
+ - "lsblk"
+ - "blkid (when I(label) or I(uuid) options are used)"
+
+author: Jan Pokorny (@japokorn)
+'''
+
+EXAMPLES = '''
+
+- name: Create LUKS container (remains unchanged if it already exists)
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ state: "present"
+ keyfile: "/vault/keyfile"
+
+- name: Create LUKS container with a passphrase
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ state: "present"
+ passphrase: "foo"
+
+- name: Create LUKS container with specific encryption
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ state: "present"
+ cipher: "aes"
+ hash: "sha256"
+
+- name: (Create and) open the LUKS container; name it "mycrypt"
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ state: "opened"
+ name: "mycrypt"
+ keyfile: "/vault/keyfile"
+
+- name: Close the existing LUKS container "mycrypt"
+ community.crypto.luks_device:
+ state: "closed"
+ name: "mycrypt"
+
+- name: Make sure LUKS container exists and is closed
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ state: "closed"
+ keyfile: "/vault/keyfile"
+
+- name: Create container if it does not exist and add new key to it
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ state: "present"
+ keyfile: "/vault/keyfile"
+ new_keyfile: "/vault/keyfile2"
+
+- name: Add new key to the LUKS container (container has to exist)
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ keyfile: "/vault/keyfile"
+ new_keyfile: "/vault/keyfile2"
+
+- name: Add new passphrase to the LUKS container
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ keyfile: "/vault/keyfile"
+ new_passphrase: "foo"
+
+- name: Remove existing keyfile from the LUKS container
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ remove_keyfile: "/vault/keyfile2"
+
+- name: Remove existing passphrase from the LUKS container
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ remove_passphrase: "foo"
+
+- name: Completely remove the LUKS container and its contents
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ state: "absent"
+
+- name: Create a container with label
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ state: "present"
+ keyfile: "/vault/keyfile"
+ label: personalLabelName
+
+- name: Open the LUKS container based on label without device; name it "mycrypt"
+ community.crypto.luks_device:
+ label: "personalLabelName"
+ state: "opened"
+ name: "mycrypt"
+ keyfile: "/vault/keyfile"
+
+- name: Close container based on UUID
+ community.crypto.luks_device:
+ uuid: 03ecd578-fad4-4e6c-9348-842e3e8fa340
+ state: "closed"
+ name: "mycrypt"
+
+- name: Create a container using luks2 format
+ community.crypto.luks_device:
+ device: "/dev/loop0"
+ state: "present"
+ keyfile: "/vault/keyfile"
+ type: luks2
+'''
+
+RETURN = '''
+name:
+ description:
+ When I(state=opened) returns (generated or given) name
+ of LUKS container. Returns None if no name is supplied.
+ returned: success
+ type: str
+ sample: "luks-c1da9a58-2fde-4256-9d9f-6ab008b4dd1b"
+'''
+
+import os
+import re
+import stat
+
+from ansible.module_utils.basic import AnsibleModule
+
+RETURN_CODE = 0
+STDOUT = 1
+STDERR = 2
+
+# used to get <luks-name> out of lsblk output in format 'crypt <luks-name>'
+# regex takes care of any possible blank characters
+LUKS_NAME_REGEX = re.compile(r'^crypt\s+([^\s]*)\s*$')
+# used to get </luks/device> out of lsblk output
+# in format 'device: </luks/device>'
+LUKS_DEVICE_REGEX = re.compile(r'\s*device:\s+([^\s]*)\s*')
+
+
+# See https://gitlab.com/cryptsetup/cryptsetup/-/wikis/LUKS-standard/on-disk-format.pdf
+LUKS_HEADER = b'LUKS\xba\xbe'
+LUKS_HEADER_L = 6
+# See https://gitlab.com/cryptsetup/LUKS2-docs/-/blob/master/luks2_doc_wip.pdf
+LUKS2_HEADER_OFFSETS = [0x4000, 0x8000, 0x10000, 0x20000, 0x40000, 0x80000, 0x100000, 0x200000, 0x400000]
+LUKS2_HEADER2 = b'SKUL\xba\xbe'
+
+
+def wipe_luks_headers(device):
+ wipe_offsets = []
+ with open(device, 'rb') as f:
+ # f.seek(0)
+ data = f.read(LUKS_HEADER_L)
+ if data == LUKS_HEADER:
+ wipe_offsets.append(0)
+ for offset in LUKS2_HEADER_OFFSETS:
+ f.seek(offset)
+ data = f.read(LUKS_HEADER_L)
+ if data == LUKS2_HEADER2:
+ wipe_offsets.append(offset)
+
+ if wipe_offsets:
+ with open(device, 'wb') as f:
+ for offset in wipe_offsets:
+ f.seek(offset)
+ f.write(b'\x00\x00\x00\x00\x00\x00')
+
+
+class Handler(object):
+
+ def __init__(self, module):
+ self._module = module
+ self._lsblk_bin = self._module.get_bin_path('lsblk', True)
+
+ def _run_command(self, command, data=None):
+ return self._module.run_command(command, data=data)
+
+ def get_device_by_uuid(self, uuid):
+ ''' Returns the device that holds UUID passed by user
+ '''
+ self._blkid_bin = self._module.get_bin_path('blkid', True)
+ uuid = self._module.params['uuid']
+ if uuid is None:
+ return None
+ result = self._run_command([self._blkid_bin, '--uuid', uuid])
+ if result[RETURN_CODE] != 0:
+ return None
+ return result[STDOUT].strip()
+
+ def get_device_by_label(self, label):
+ ''' Returns the device that holds label passed by user
+ '''
+ self._blkid_bin = self._module.get_bin_path('blkid', True)
+ label = self._module.params['label']
+ if label is None:
+ return None
+ result = self._run_command([self._blkid_bin, '--label', label])
+ if result[RETURN_CODE] != 0:
+ return None
+ return result[STDOUT].strip()
+
+ def generate_luks_name(self, device):
+ ''' Generate name for luks based on device UUID ('luks-<UUID>').
+ Raises ValueError when obtaining of UUID fails.
+ '''
+ result = self._run_command([self._lsblk_bin, '-n', device, '-o', 'UUID'])
+
+ if result[RETURN_CODE] != 0:
+ raise ValueError('Error while generating LUKS name for %s: %s'
+ % (device, result[STDERR]))
+ dev_uuid = result[STDOUT].strip()
+ return 'luks-%s' % dev_uuid
+
+
+class CryptHandler(Handler):
+
+ def __init__(self, module):
+ super(CryptHandler, self).__init__(module)
+ self._cryptsetup_bin = self._module.get_bin_path('cryptsetup', True)
+
+ def get_container_name_by_device(self, device):
+ ''' obtain LUKS container name based on the device where it is located
+ return None if not found
+ raise ValueError if lsblk command fails
+ '''
+ result = self._run_command([self._lsblk_bin, device, '-nlo', 'type,name'])
+ if result[RETURN_CODE] != 0:
+ raise ValueError('Error while obtaining LUKS name for %s: %s'
+ % (device, result[STDERR]))
+
+ for line in result[STDOUT].splitlines(False):
+ m = LUKS_NAME_REGEX.match(line)
+ if m:
+ return m.group(1)
+ return None
+
+ def get_container_device_by_name(self, name):
+ ''' obtain device name based on the LUKS container name
+ return None if not found
+ raise ValueError if lsblk command fails
+ '''
+ # apparently each device can have only one LUKS container on it
+ result = self._run_command([self._cryptsetup_bin, 'status', name])
+ if result[RETURN_CODE] != 0:
+ return None
+
+ m = LUKS_DEVICE_REGEX.search(result[STDOUT])
+ device = m.group(1)
+ return device
+
+ def is_luks(self, device):
+ ''' check if the LUKS container does exist
+ '''
+ result = self._run_command([self._cryptsetup_bin, 'isLuks', device])
+ return result[RETURN_CODE] == 0
+
+ def _add_pbkdf_options(self, options, pbkdf):
+ if pbkdf['iteration_time'] is not None:
+ options.extend(['--iter-time', str(int(pbkdf['iteration_time'] * 1000))])
+ if pbkdf['iteration_count'] is not None:
+ options.extend(['--pbkdf-force-iterations', str(pbkdf['iteration_count'])])
+ if pbkdf['algorithm'] is not None:
+ options.extend(['--pbkdf', pbkdf['algorithm']])
+ if pbkdf['memory'] is not None:
+ options.extend(['--pbkdf-memory', str(pbkdf['memory'])])
+ if pbkdf['parallel'] is not None:
+ options.extend(['--pbkdf-parallel', str(pbkdf['parallel'])])
+
+ def run_luks_create(self, device, keyfile, passphrase, keysize, cipher, hash_, sector_size, pbkdf):
+ # create a new luks container; use batch mode to auto confirm
+ luks_type = self._module.params['type']
+ label = self._module.params['label']
+
+ options = []
+ if keysize is not None:
+ options.append('--key-size=' + str(keysize))
+ if label is not None:
+ options.extend(['--label', label])
+ luks_type = 'luks2'
+ if luks_type is not None:
+ options.extend(['--type', luks_type])
+ if cipher is not None:
+ options.extend(['--cipher', cipher])
+ if hash_ is not None:
+ options.extend(['--hash', hash_])
+ if pbkdf is not None:
+ self._add_pbkdf_options(options, pbkdf)
+ if sector_size is not None:
+ options.extend(['--sector-size', str(sector_size)])
+
+ args = [self._cryptsetup_bin, 'luksFormat']
+ args.extend(options)
+ args.extend(['-q', device])
+ if keyfile:
+ args.append(keyfile)
+
+ result = self._run_command(args, data=passphrase)
+ if result[RETURN_CODE] != 0:
+ raise ValueError('Error while creating LUKS on %s: %s'
+ % (device, result[STDERR]))
+
+ def run_luks_open(self, device, keyfile, passphrase, perf_same_cpu_crypt, perf_submit_from_crypt_cpus,
+ perf_no_read_workqueue, perf_no_write_workqueue, persistent, name):
+ args = [self._cryptsetup_bin]
+ if keyfile:
+ args.extend(['--key-file', keyfile])
+ if perf_same_cpu_crypt:
+ args.extend(['--perf-same_cpu_crypt'])
+ if perf_submit_from_crypt_cpus:
+ args.extend(['--perf-submit_from_crypt_cpus'])
+ if perf_no_read_workqueue:
+ args.extend(['--perf-no_read_workqueue'])
+ if perf_no_write_workqueue:
+ args.extend(['--perf-no_write_workqueue'])
+ if persistent:
+ args.extend(['--persistent'])
+ args.extend(['open', '--type', 'luks', device, name])
+
+ result = self._run_command(args, data=passphrase)
+ if result[RETURN_CODE] != 0:
+ raise ValueError('Error while opening LUKS container on %s: %s'
+ % (device, result[STDERR]))
+
+ def run_luks_close(self, name):
+ result = self._run_command([self._cryptsetup_bin, 'close', name])
+ if result[RETURN_CODE] != 0:
+ raise ValueError('Error while closing LUKS container %s' % (name))
+
+ def run_luks_remove(self, device):
+ wipefs_bin = self._module.get_bin_path('wipefs', True)
+
+ name = self.get_container_name_by_device(device)
+ if name is not None:
+ self.run_luks_close(name)
+ result = self._run_command([wipefs_bin, '--all', device])
+ if result[RETURN_CODE] != 0:
+ raise ValueError('Error while wiping LUKS container signatures for %s: %s'
+ % (device, result[STDERR]))
+
+ # For LUKS2, sometimes both `cryptsetup erase` and `wipefs` do **not**
+ # erase all LUKS signatures (they seem to miss the second header). That's
+ # why we do it ourselves here.
+ try:
+ wipe_luks_headers(device)
+ except Exception as exc:
+ raise ValueError('Error while wiping LUKS container signatures for %s: %s' % (device, exc))
+
+ def run_luks_add_key(self, device, keyfile, passphrase, new_keyfile,
+ new_passphrase, pbkdf):
+ ''' Add new key from a keyfile or passphrase to given 'device';
+ authentication done using 'keyfile' or 'passphrase'.
+ Raises ValueError when command fails.
+ '''
+ data = []
+ args = [self._cryptsetup_bin, 'luksAddKey', device]
+ if pbkdf is not None:
+ self._add_pbkdf_options(args, pbkdf)
+
+ if keyfile:
+ args.extend(['--key-file', keyfile])
+ else:
+ data.append(passphrase)
+
+ if new_keyfile:
+ args.append(new_keyfile)
+ else:
+ data.extend([new_passphrase, new_passphrase])
+
+ result = self._run_command(args, data='\n'.join(data) or None)
+ if result[RETURN_CODE] != 0:
+ raise ValueError('Error while adding new LUKS keyslot to %s: %s'
+ % (device, result[STDERR]))
+
+ def run_luks_remove_key(self, device, keyfile, passphrase,
+ force_remove_last_key=False):
+ ''' Remove key from given device
+ Raises ValueError when command fails
+ '''
+ if not force_remove_last_key:
+ result = self._run_command([self._cryptsetup_bin, 'luksDump', device])
+ if result[RETURN_CODE] != 0:
+ raise ValueError('Error while dumping LUKS header from %s'
+ % (device, ))
+ keyslot_count = 0
+ keyslot_area = False
+ keyslot_re = re.compile(r'^Key Slot [0-9]+: ENABLED')
+ for line in result[STDOUT].splitlines():
+ if line.startswith('Keyslots:'):
+ keyslot_area = True
+ elif line.startswith(' '):
+ # LUKS2 header dumps use human-readable indented output.
+ # Thus we have to look out for 'Keyslots:' and count the
+ # number of indented keyslot numbers.
+ if keyslot_area and line[2] in '0123456789':
+ keyslot_count += 1
+ elif line.startswith('\t'):
+ pass
+ elif keyslot_re.match(line):
+ # LUKS1 header dumps have one line per keyslot with ENABLED
+ # or DISABLED in them. We count such lines with ENABLED.
+ keyslot_count += 1
+ else:
+ keyslot_area = False
+ if keyslot_count < 2:
+ self._module.fail_json(msg="LUKS device %s has less than two active keyslots. "
+ "To be able to remove a key, please set "
+ "`force_remove_last_key` to `true`." % device)
+
+ args = [self._cryptsetup_bin, 'luksRemoveKey', device, '-q']
+ if keyfile:
+ args.extend(['--key-file', keyfile])
+ result = self._run_command(args, data=passphrase)
+ if result[RETURN_CODE] != 0:
+ raise ValueError('Error while removing LUKS key from %s: %s'
+ % (device, result[STDERR]))
+
+ def luks_test_key(self, device, keyfile, passphrase):
+ ''' Check whether the keyfile or passphrase works.
+ Raises ValueError when command fails.
+ '''
+ data = None
+ args = [self._cryptsetup_bin, 'luksOpen', '--test-passphrase', device]
+
+ if keyfile:
+ args.extend(['--key-file', keyfile])
+ else:
+ data = passphrase
+
+ result = self._run_command(args, data=data)
+ if result[RETURN_CODE] == 0:
+ return True
+ for output in (STDOUT, STDERR):
+ if 'No key available with this passphrase' in result[output]:
+ return False
+
+ raise ValueError('Error while testing whether keyslot exists on %s: %s'
+ % (device, result[STDERR]))
+
+
+class ConditionsHandler(Handler):
+
+ def __init__(self, module, crypthandler):
+ super(ConditionsHandler, self).__init__(module)
+ self._crypthandler = crypthandler
+ self.device = self.get_device_name()
+
+ def get_device_name(self):
+ device = self._module.params.get('device')
+ label = self._module.params.get('label')
+ uuid = self._module.params.get('uuid')
+ name = self._module.params.get('name')
+
+ if device is None and label is not None:
+ device = self.get_device_by_label(label)
+ elif device is None and uuid is not None:
+ device = self.get_device_by_uuid(uuid)
+ elif device is None and name is not None:
+ device = self._crypthandler.get_container_device_by_name(name)
+
+ return device
+
+ def luks_create(self):
+ return (self.device is not None and
+ (self._module.params['keyfile'] is not None or
+ self._module.params['passphrase'] is not None) and
+ self._module.params['state'] in ('present',
+ 'opened',
+ 'closed') and
+ not self._crypthandler.is_luks(self.device))
+
+ def opened_luks_name(self):
+ ''' If luks is already opened, return its name.
+ If 'name' parameter is specified and differs
+ from obtained value, fail.
+ Return None otherwise
+ '''
+ if self._module.params['state'] != 'opened':
+ return None
+
+ # try to obtain luks name - it may be already opened
+ name = self._crypthandler.get_container_name_by_device(self.device)
+
+ if name is None:
+ # container is not open
+ return None
+
+ if self._module.params['name'] is None:
+ # container is already opened
+ return name
+
+ if name != self._module.params['name']:
+ # the container is already open but with different name:
+ # suspicious. back off
+ self._module.fail_json(msg="LUKS container is already opened "
+ "under different name '%s'." % name)
+
+ # container is opened and the names match
+ return name
+
+ def luks_open(self):
+ if ((self._module.params['keyfile'] is None and
+ self._module.params['passphrase'] is None) or
+ self.device is None or
+ self._module.params['state'] != 'opened'):
+ # conditions for open not fulfilled
+ return False
+
+ name = self.opened_luks_name()
+
+ if name is None:
+ return True
+ return False
+
+ def luks_close(self):
+ if ((self._module.params['name'] is None and self.device is None) or
+ self._module.params['state'] != 'closed'):
+ # conditions for close not fulfilled
+ return False
+
+ if self.device is not None:
+ name = self._crypthandler.get_container_name_by_device(self.device)
+ # successfully getting name based on device means that luks is open
+ luks_is_open = name is not None
+
+ if self._module.params['name'] is not None:
+ self.device = self._crypthandler.get_container_device_by_name(
+ self._module.params['name'])
+ # successfully getting device based on name means that luks is open
+ luks_is_open = self.device is not None
+
+ return luks_is_open
+
+ def luks_add_key(self):
+ if (self.device is None or
+ (self._module.params['keyfile'] is None and
+ self._module.params['passphrase'] is None) or
+ (self._module.params['new_keyfile'] is None and
+ self._module.params['new_passphrase'] is None)):
+ # conditions for adding a key not fulfilled
+ return False
+
+ if self._module.params['state'] == 'absent':
+ self._module.fail_json(msg="Contradiction in setup: Asking to "
+ "add a key to absent LUKS.")
+
+ return not self._crypthandler.luks_test_key(self.device, self._module.params['new_keyfile'], self._module.params['new_passphrase'])
+
+ def luks_remove_key(self):
+ if (self.device is None or
+ (self._module.params['remove_keyfile'] is None and
+ self._module.params['remove_passphrase'] is None)):
+ # conditions for removing a key not fulfilled
+ return False
+
+ if self._module.params['state'] == 'absent':
+ self._module.fail_json(msg="Contradiction in setup: Asking to "
+ "remove a key from absent LUKS.")
+
+ return self._crypthandler.luks_test_key(self.device, self._module.params['remove_keyfile'], self._module.params['remove_passphrase'])
+
+ def luks_remove(self):
+ return (self.device is not None and
+ self._module.params['state'] == 'absent' and
+ self._crypthandler.is_luks(self.device))
+
+
+def run_module():
+ # available arguments/parameters that a user can pass
+ module_args = dict(
+ state=dict(type='str', default='present', choices=['present', 'absent', 'opened', 'closed']),
+ device=dict(type='str'),
+ name=dict(type='str'),
+ keyfile=dict(type='path'),
+ new_keyfile=dict(type='path'),
+ remove_keyfile=dict(type='path'),
+ passphrase=dict(type='str', no_log=True),
+ new_passphrase=dict(type='str', no_log=True),
+ remove_passphrase=dict(type='str', no_log=True),
+ force_remove_last_key=dict(type='bool', default=False),
+ keysize=dict(type='int'),
+ label=dict(type='str'),
+ uuid=dict(type='str'),
+ type=dict(type='str', choices=['luks1', 'luks2']),
+ cipher=dict(type='str'),
+ hash=dict(type='str'),
+ pbkdf=dict(
+ type='dict',
+ options=dict(
+ iteration_time=dict(type='float'),
+ iteration_count=dict(type='int'),
+ algorithm=dict(type='str', choices=['argon2i', 'argon2id', 'pbkdf2']),
+ memory=dict(type='int'),
+ parallel=dict(type='int'),
+ ),
+ mutually_exclusive=[('iteration_time', 'iteration_count')],
+ ),
+ sector_size=dict(type='int'),
+ perf_same_cpu_crypt=dict(type='bool', default=False),
+ perf_submit_from_crypt_cpus=dict(type='bool', default=False),
+ perf_no_read_workqueue=dict(type='bool', default=False),
+ perf_no_write_workqueue=dict(type='bool', default=False),
+ persistent=dict(type='bool', default=False),
+ )
+
+ mutually_exclusive = [
+ ('keyfile', 'passphrase'),
+ ('new_keyfile', 'new_passphrase'),
+ ('remove_keyfile', 'remove_passphrase')
+ ]
+
+ # seed the result dict in the object
+ result = dict(
+ changed=False,
+ name=None
+ )
+
+ module = AnsibleModule(argument_spec=module_args,
+ supports_check_mode=True,
+ mutually_exclusive=mutually_exclusive)
+ module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
+
+ if module.params['device'] is not None:
+ try:
+ statinfo = os.stat(module.params['device'])
+ mode = statinfo.st_mode
+ if not stat.S_ISBLK(mode) and not stat.S_ISCHR(mode):
+ raise Exception('{0} is not a device'.format(module.params['device']))
+ except Exception as e:
+ module.fail_json(msg=str(e))
+
+ crypt = CryptHandler(module)
+ conditions = ConditionsHandler(module, crypt)
+
+ # conditions not allowed to run
+ if module.params['label'] is not None and module.params['type'] == 'luks1':
+ module.fail_json(msg='You cannot combine type luks1 with the label option.')
+
+ # The conditions are in order to allow more operations in one run.
+ # (e.g. create luks and add a key to it)
+
+ # luks create
+ if conditions.luks_create():
+ if not module.check_mode:
+ try:
+ crypt.run_luks_create(conditions.device,
+ module.params['keyfile'],
+ module.params['passphrase'],
+ module.params['keysize'],
+ module.params['cipher'],
+ module.params['hash'],
+ module.params['sector_size'],
+ module.params['pbkdf'],
+ )
+ except ValueError as e:
+ module.fail_json(msg="luks_device error: %s" % e)
+ result['changed'] = True
+ if module.check_mode:
+ module.exit_json(**result)
+
+ # luks open
+
+ name = conditions.opened_luks_name()
+ if name is not None:
+ result['name'] = name
+
+ if conditions.luks_open():
+ name = module.params['name']
+ if name is None:
+ try:
+ name = crypt.generate_luks_name(conditions.device)
+ except ValueError as e:
+ module.fail_json(msg="luks_device error: %s" % e)
+ if not module.check_mode:
+ try:
+ crypt.run_luks_open(conditions.device,
+ module.params['keyfile'],
+ module.params['passphrase'],
+ module.params['perf_same_cpu_crypt'],
+ module.params['perf_submit_from_crypt_cpus'],
+ module.params['perf_no_read_workqueue'],
+ module.params['perf_no_write_workqueue'],
+ module.params['persistent'],
+ name)
+ except ValueError as e:
+ module.fail_json(msg="luks_device error: %s" % e)
+ result['name'] = name
+ result['changed'] = True
+ if module.check_mode:
+ module.exit_json(**result)
+
+ # luks close
+ if conditions.luks_close():
+ if conditions.device is not None:
+ try:
+ name = crypt.get_container_name_by_device(
+ conditions.device)
+ except ValueError as e:
+ module.fail_json(msg="luks_device error: %s" % e)
+ else:
+ name = module.params['name']
+ if not module.check_mode:
+ try:
+ crypt.run_luks_close(name)
+ except ValueError as e:
+ module.fail_json(msg="luks_device error: %s" % e)
+ result['name'] = name
+ result['changed'] = True
+ if module.check_mode:
+ module.exit_json(**result)
+
+ # luks add key
+ if conditions.luks_add_key():
+ if not module.check_mode:
+ try:
+ crypt.run_luks_add_key(conditions.device,
+ module.params['keyfile'],
+ module.params['passphrase'],
+ module.params['new_keyfile'],
+ module.params['new_passphrase'],
+ module.params['pbkdf'])
+ except ValueError as e:
+ module.fail_json(msg="luks_device error: %s" % e)
+ result['changed'] = True
+ if module.check_mode:
+ module.exit_json(**result)
+
+ # luks remove key
+ if conditions.luks_remove_key():
+ if not module.check_mode:
+ try:
+ last_key = module.params['force_remove_last_key']
+ crypt.run_luks_remove_key(conditions.device,
+ module.params['remove_keyfile'],
+ module.params['remove_passphrase'],
+ force_remove_last_key=last_key)
+ except ValueError as e:
+ module.fail_json(msg="luks_device error: %s" % e)
+ result['changed'] = True
+ if module.check_mode:
+ module.exit_json(**result)
+
+ # luks remove
+ if conditions.luks_remove():
+ if not module.check_mode:
+ try:
+ crypt.run_luks_remove(conditions.device)
+ except ValueError as e:
+ module.fail_json(msg="luks_device error: %s" % e)
+ result['changed'] = True
+ if module.check_mode:
+ module.exit_json(**result)
+
+ # Success - return result
+ module.exit_json(**result)
+
+
+def main():
+ run_module()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssh_cert.py b/ansible_collections/community/crypto/plugins/modules/openssh_cert.py
new file mode 100644
index 00000000..8f428107
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssh_cert.py
@@ -0,0 +1,578 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = '''
+---
+module: openssh_cert
+author: "David Kainz (@lolcube)"
+short_description: Generate OpenSSH host or user certificates.
+description:
+ - Generate and regenerate OpenSSH host or user certificates.
+requirements:
+ - "ssh-keygen"
+extends_documentation_fragment:
+ - ansible.builtin.files
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+ safe_file_operations:
+ support: full
+options:
+ state:
+ description:
+ - Whether the host or user certificate should exist or not, taking action if the state is different
+ from what is stated.
+ type: str
+ default: "present"
+ choices: [ 'present', 'absent' ]
+ type:
+ description:
+ - Whether the module should generate a host or a user certificate.
+ - Required if I(state) is C(present).
+ type: str
+ choices: ['host', 'user']
+ force:
+ description:
+ - Should the certificate be regenerated even if it already exists and is valid.
+ - Equivalent to I(regenerate=always).
+ type: bool
+ default: false
+ path:
+ description:
+ - Path of the file containing the certificate.
+ type: path
+ required: true
+ regenerate:
+ description:
+ - When C(never) the task will fail if a certificate already exists at I(path) and is unreadable
+ otherwise a new certificate will only be generated if there is no existing certificate.
+ - When C(fail) the task will fail if a certificate already exists at I(path) and does not
+ match the module's options.
+ - When C(partial_idempotence) an existing certificate will be regenerated based on
+ I(serial), I(signature_algorithm), I(type), I(valid_from), I(valid_to), I(valid_at), and I(principals).
+ I(valid_from) and I(valid_to) can be excluded by I(ignore_timestamps=true).
+ - When C(full_idempotence) I(identifier), I(options), I(public_key), and I(signing_key)
+ are also considered when compared against an existing certificate.
+ - C(always) is equivalent to I(force=true).
+ type: str
+ choices:
+ - never
+ - fail
+ - partial_idempotence
+ - full_idempotence
+ - always
+ default: partial_idempotence
+ version_added: 1.8.0
+ signature_algorithm:
+ description:
+ - As of OpenSSH 8.2 the SHA-1 signature algorithm for RSA keys has been disabled and C(ssh) will refuse
+ host certificates signed with the SHA-1 algorithm. OpenSSH 8.1 made C(rsa-sha2-512) the default algorithm
+ when acting as a CA and signing certificates with a RSA key. However, for OpenSSH versions less than 8.1
+ the SHA-2 signature algorithms, C(rsa-sha2-256) or C(rsa-sha2-512), must be specified using this option
+ if compatibility with newer C(ssh) clients is required. Conversely if hosts using OpenSSH version 8.2
+ or greater must remain compatible with C(ssh) clients using OpenSSH less than 7.2, then C(ssh-rsa)
+ can be used when generating host certificates (a corresponding change to the sshd_config to add C(ssh-rsa)
+ to the C(CASignatureAlgorithms) keyword is also required).
+ - Using any value for this option with a non-RSA I(signing_key) will cause this module to fail.
+ - "Note: OpenSSH versions prior to 7.2 do not support SHA-2 signature algorithms for RSA keys and OpenSSH
+ versions prior to 7.3 do not support SHA-2 signature algorithms for certificates."
+ - See U(https://www.openssh.com/txt/release-8.2) for more information.
+ type: str
+ choices:
+ - ssh-rsa
+ - rsa-sha2-256
+ - rsa-sha2-512
+ version_added: 1.10.0
+ signing_key:
+ description:
+ - The path to the private openssh key that is used for signing the public key in order to generate the certificate.
+ - If the private key is on a PKCS#11 token (I(pkcs11_provider)), set this to the path to the public key instead.
+ - Required if I(state) is C(present).
+ type: path
+ pkcs11_provider:
+ description:
+ - To use a signing key that resides on a PKCS#11 token, set this to the name (or full path) of the shared library to use with the token.
+ Usually C(libpkcs11.so).
+ - If this is set, I(signing_key) needs to point to a file containing the public key of the CA.
+ type: str
+ version_added: 1.1.0
+ use_agent:
+ description:
+ - Should the ssh-keygen use a CA key residing in a ssh-agent.
+ type: bool
+ default: false
+ version_added: 1.3.0
+ public_key:
+ description:
+ - The path to the public key that will be signed with the signing key in order to generate the certificate.
+ - Required if I(state) is C(present).
+ type: path
+ valid_from:
+ description:
+ - "The point in time the certificate is valid from. Time can be specified either as relative time or as absolute timestamp.
+ Time will always be interpreted as UTC. Valid formats are: C([+-]timespec | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS | YYYY-MM-DD HH:MM:SS | always)
+ where timespec can be an integer + C([w | d | h | m | s]) (for example C(+32w1d2h)).
+ Note that if using relative time this module is NOT idempotent."
+ - "The value C(always) is only supported for OpenSSH 7.7 and greater, however, the value C(1970-01-01T00:00:01)
+ can be used with earlier versions as an equivalent expression."
+ - "To ignore this value during comparison with an existing certificate set I(ignore_timestamps=true)."
+ - Required if I(state) is C(present).
+ type: str
+ valid_to:
+ description:
+ - "The point in time the certificate is valid to. Time can be specified either as relative time or as absolute timestamp.
+ Time will always be interpreted as UTC. Valid formats are: C([+-]timespec | YYYY-MM-DD | YYYY-MM-DDTHH:MM:SS | YYYY-MM-DD HH:MM:SS | forever)
+ where timespec can be an integer + C([w | d | h | m | s]) (for example C(+32w1d2h)).
+ Note that if using relative time this module is NOT idempotent."
+ - "To ignore this value during comparison with an existing certificate set I(ignore_timestamps=true)."
+ - Required if I(state) is C(present).
+ type: str
+ valid_at:
+ description:
+ - "Check if the certificate is valid at a certain point in time. If it is not the certificate will be regenerated.
+ Time will always be interpreted as UTC. Mainly to be used with relative timespec for I(valid_from) and / or I(valid_to).
+ Note that if using relative time this module is NOT idempotent."
+ type: str
+ ignore_timestamps:
+ description:
+ - "Whether the I(valid_from) and I(valid_to) timestamps should be ignored for idempotency checks."
+ - "However, the values will still be applied to a new certificate if it meets any other necessary conditions for generation/regeneration."
+ type: bool
+ default: false
+ version_added: 2.2.0
+ principals:
+ description:
+ - "Certificates may be limited to be valid for a set of principal (user/host) names.
+ By default, generated certificates are valid for all users or hosts."
+ type: list
+ elements: str
+ options:
+ description:
+ - "Specify certificate options when signing a key. The option that are valid for user certificates are:"
+ - "C(clear): Clear all enabled permissions. This is useful for clearing the default set of permissions so permissions may be added individually."
+ - "C(force-command=command): Forces the execution of command instead of any shell or
+ command specified by the user when the certificate is used for authentication."
+ - "C(no-agent-forwarding): Disable ssh-agent forwarding (permitted by default)."
+ - "C(no-port-forwarding): Disable port forwarding (permitted by default)."
+ - "C(no-pty): Disable PTY allocation (permitted by default)."
+ - "C(no-user-rc): Disable execution of C(~/.ssh/rc) by sshd (permitted by default)."
+ - "C(no-x11-forwarding): Disable X11 forwarding (permitted by default)"
+ - "C(permit-agent-forwarding): Allows ssh-agent forwarding."
+ - "C(permit-port-forwarding): Allows port forwarding."
+ - "C(permit-pty): Allows PTY allocation."
+ - "C(permit-user-rc): Allows execution of C(~/.ssh/rc) by sshd."
+ - "C(permit-x11-forwarding): Allows X11 forwarding."
+ - "C(source-address=address_list): Restrict the source addresses from which the certificate is considered valid.
+ The C(address_list) is a comma-separated list of one or more address/netmask pairs in CIDR format."
+ - "At present, no options are valid for host keys."
+ type: list
+ elements: str
+ identifier:
+ description:
+ - Specify the key identity when signing a public key. The identifier that is logged by the server when the certificate is used for authentication.
+ type: str
+ serial_number:
+ description:
+ - "Specify the certificate serial number.
+ The serial number is logged by the server when the certificate is used for authentication.
+ The certificate serial number may be used in a KeyRevocationList.
+ The serial number may be omitted for checks, but must be specified again for a new certificate.
+ Note: The default value set by ssh-keygen is 0."
+ type: int
+'''
+
+EXAMPLES = '''
+- name: Generate an OpenSSH user certificate that is valid forever and for all users
+ community.crypto.openssh_cert:
+ type: user
+ signing_key: /path/to/private_key
+ public_key: /path/to/public_key.pub
+ path: /path/to/certificate
+ valid_from: always
+ valid_to: forever
+
+# Generate an OpenSSH host certificate that is valid for 32 weeks from now and will be regenerated
+# if it is valid for less than 2 weeks from the time the module is being run
+- name: Generate an OpenSSH host certificate with valid_from, valid_to and valid_at parameters
+ community.crypto.openssh_cert:
+ type: host
+ signing_key: /path/to/private_key
+ public_key: /path/to/public_key.pub
+ path: /path/to/certificate
+ valid_from: +0s
+ valid_to: +32w
+ valid_at: +2w
+ ignore_timestamps: true
+
+- name: Generate an OpenSSH host certificate that is valid forever and only for example.com and examplehost
+ community.crypto.openssh_cert:
+ type: host
+ signing_key: /path/to/private_key
+ public_key: /path/to/public_key.pub
+ path: /path/to/certificate
+ valid_from: always
+ valid_to: forever
+ principals:
+ - example.com
+ - examplehost
+
+- name: Generate an OpenSSH host Certificate that is valid from 21.1.2001 to 21.1.2019
+ community.crypto.openssh_cert:
+ type: host
+ signing_key: /path/to/private_key
+ public_key: /path/to/public_key.pub
+ path: /path/to/certificate
+ valid_from: "2001-01-21"
+ valid_to: "2019-01-21"
+
+- name: Generate an OpenSSH user Certificate with clear and force-command option
+ community.crypto.openssh_cert:
+ type: user
+ signing_key: /path/to/private_key
+ public_key: /path/to/public_key.pub
+ path: /path/to/certificate
+ valid_from: always
+ valid_to: forever
+ options:
+ - "clear"
+ - "force-command=/tmp/bla/foo"
+
+- name: Generate an OpenSSH user certificate using a PKCS#11 token
+ community.crypto.openssh_cert:
+ type: user
+ signing_key: /path/to/ca_public_key.pub
+ pkcs11_provider: libpkcs11.so
+ public_key: /path/to/public_key.pub
+ path: /path/to/certificate
+ valid_from: always
+ valid_to: forever
+
+'''
+
+RETURN = '''
+type:
+ description: type of the certificate (host or user)
+ returned: changed or success
+ type: str
+ sample: host
+filename:
+ description: path to the certificate
+ returned: changed or success
+ type: str
+ sample: /tmp/certificate-cert.pub
+info:
+ description: Information about the certificate. Output of C(ssh-keygen -L -f).
+ returned: change or success
+ type: list
+ elements: str
+
+'''
+
+import os
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.common import (
+ KeygenCommand,
+ OpensshModule,
+ PrivateKey,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.openssh.certificate import (
+ OpensshCertificate,
+ OpensshCertificateTimeParameters,
+ parse_option_list,
+)
+
+
+class Certificate(OpensshModule):
+ def __init__(self, module):
+ super(Certificate, self).__init__(module)
+ self.ssh_keygen = KeygenCommand(self.module)
+
+ self.identifier = self.module.params['identifier'] or ""
+ self.options = self.module.params['options'] or []
+ self.path = self.module.params['path']
+ self.pkcs11_provider = self.module.params['pkcs11_provider']
+ self.principals = self.module.params['principals'] or []
+ self.public_key = self.module.params['public_key']
+ self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always'
+ self.serial_number = self.module.params['serial_number']
+ self.signature_algorithm = self.module.params['signature_algorithm']
+ self.signing_key = self.module.params['signing_key']
+ self.state = self.module.params['state']
+ self.type = self.module.params['type']
+ self.use_agent = self.module.params['use_agent']
+ self.valid_at = self.module.params['valid_at']
+ self.ignore_timestamps = self.module.params['ignore_timestamps']
+
+ self._check_if_base_dir(self.path)
+
+ if self.state == 'present':
+ self._validate_parameters()
+
+ self.data = None
+ self.original_data = None
+ if self._exists():
+ self._load_certificate()
+
+ self.time_parameters = None
+ if self.state == 'present':
+ self._set_time_parameters()
+
+ def _validate_parameters(self):
+ for path in (self.public_key, self.signing_key):
+ self._check_if_base_dir(path)
+
+ if self.options and self.type == "host":
+ self.module.fail_json(msg="Options can only be used with user certificates.")
+
+ if self.use_agent:
+ self._use_agent_available()
+
+ def _use_agent_available(self):
+ ssh_version = self._get_ssh_version()
+ if not ssh_version:
+ self.module.fail_json(msg="Failed to determine ssh version")
+ elif LooseVersion(ssh_version) < LooseVersion("7.6"):
+ self.module.fail_json(
+ msg="Signing with CA key in ssh agent requires ssh 7.6 or newer." +
+ " Your version is: %s" % ssh_version
+ )
+
+ def _exists(self):
+ return os.path.exists(self.path)
+
+ def _load_certificate(self):
+ try:
+ self.original_data = OpensshCertificate.load(self.path)
+ except (TypeError, ValueError) as e:
+ if self.regenerate in ('never', 'fail'):
+ self.module.fail_json(msg="Unable to read existing certificate: %s" % to_native(e))
+ self.module.warn("Unable to read existing certificate: %s" % to_native(e))
+
+ def _set_time_parameters(self):
+ try:
+ self.time_parameters = OpensshCertificateTimeParameters(
+ valid_from=self.module.params['valid_from'],
+ valid_to=self.module.params['valid_to'],
+ )
+ except ValueError as e:
+ self.module.fail_json(msg=to_native(e))
+
+ def _execute(self):
+ if self.state == 'present':
+ if self._should_generate():
+ self._generate()
+ self._update_permissions(self.path)
+ else:
+ if self._exists():
+ self._remove()
+
+ def _should_generate(self):
+ if self.regenerate == 'never':
+ return self.original_data is None
+ elif self.regenerate == 'fail':
+ if self.original_data and not self._is_fully_valid():
+ self.module.fail_json(
+ msg="Certificate does not match the provided options.",
+ cert=get_cert_dict(self.original_data)
+ )
+ return self.original_data is None
+ elif self.regenerate == 'partial_idempotence':
+ return self.original_data is None or not self._is_partially_valid()
+ elif self.regenerate == 'full_idempotence':
+ return self.original_data is None or not self._is_fully_valid()
+ else:
+ return True
+
+ def _is_fully_valid(self):
+ return self._is_partially_valid() and all([
+ self._compare_options() if self.original_data.type == 'user' else True,
+ self.original_data.key_id == self.identifier,
+ self.original_data.public_key == self._get_key_fingerprint(self.public_key),
+ self.original_data.signing_key == self._get_key_fingerprint(self.signing_key),
+ ])
+
+ def _is_partially_valid(self):
+ return all([
+ set(self.original_data.principals) == set(self.principals),
+ self.original_data.signature_type == self.signature_algorithm if self.signature_algorithm else True,
+ self.original_data.serial == self.serial_number if self.serial_number is not None else True,
+ self.original_data.type == self.type,
+ self._compare_time_parameters(),
+ ])
+
+ def _compare_time_parameters(self):
+ try:
+ original_time_parameters = OpensshCertificateTimeParameters(
+ valid_from=self.original_data.valid_after,
+ valid_to=self.original_data.valid_before
+ )
+ except ValueError as e:
+ return self.module.fail_json(msg=to_native(e))
+
+ if self.ignore_timestamps:
+ return original_time_parameters.within_range(self.valid_at)
+
+ return all([
+ original_time_parameters == self.time_parameters,
+ original_time_parameters.within_range(self.valid_at)
+ ])
+
+ def _compare_options(self):
+ try:
+ critical_options, extensions = parse_option_list(self.options)
+ except ValueError as e:
+ return self.module.fail_json(msg=to_native(e))
+
+ return all([
+ set(self.original_data.critical_options) == set(critical_options),
+ set(self.original_data.extensions) == set(extensions)
+ ])
+
+ def _get_key_fingerprint(self, path):
+ private_key_content = self.ssh_keygen.get_private_key(path, check_rc=True)[1]
+ return PrivateKey.from_string(private_key_content).fingerprint
+
+ @OpensshModule.trigger_change
+ @OpensshModule.skip_if_check_mode
+ def _generate(self):
+ try:
+ temp_certificate = self._generate_temp_certificate()
+ self._safe_secure_move([(temp_certificate, self.path)])
+ except OSError as e:
+ self.module.fail_json(msg="Unable to write certificate to %s: %s" % (self.path, to_native(e)))
+
+ try:
+ self.data = OpensshCertificate.load(self.path)
+ except (TypeError, ValueError) as e:
+ self.module.fail_json(msg="Unable to read new certificate: %s" % to_native(e))
+
+ def _generate_temp_certificate(self):
+ key_copy = os.path.join(self.module.tmpdir, os.path.basename(self.public_key))
+
+ try:
+ self.module.preserved_copy(self.public_key, key_copy)
+ except OSError as e:
+ self.module.fail_json(msg="Unable to stage temporary key: %s" % to_native(e))
+ self.module.add_cleanup_file(key_copy)
+
+ self.ssh_keygen.generate_certificate(
+ key_copy, self.identifier, self.options, self.pkcs11_provider, self.principals, self.serial_number,
+ self.signature_algorithm, self.signing_key, self.type, self.time_parameters, self.use_agent,
+ environ_update=dict(TZ="UTC"), check_rc=True
+ )
+
+ temp_cert = os.path.splitext(key_copy)[0] + '-cert.pub'
+ self.module.add_cleanup_file(temp_cert)
+
+ return temp_cert
+
+ @OpensshModule.trigger_change
+ @OpensshModule.skip_if_check_mode
+ def _remove(self):
+ try:
+ os.remove(self.path)
+ except OSError as e:
+ self.module.fail_json(msg="Unable to remove existing certificate: %s" % to_native(e))
+
+ @property
+ def _result(self):
+ if self.state != 'present':
+ return {}
+
+ certificate_info = self.ssh_keygen.get_certificate_info(self.path)[1]
+
+ return {
+ 'type': self.type,
+ 'filename': self.path,
+ 'info': format_cert_info(certificate_info),
+ }
+
+ @property
+ def diff(self):
+ return {
+ 'before': get_cert_dict(self.original_data),
+ 'after': get_cert_dict(self.data)
+ }
+
+
+def format_cert_info(cert_info):
+ result = []
+ string = ""
+
+ for word in cert_info.split():
+ if word in ("Type:", "Public", "Signing", "Key", "Serial:", "Valid:", "Principals:", "Critical", "Extensions:"):
+ result.append(string)
+ string = word
+ else:
+ string += " " + word
+ result.append(string)
+ # Drop the certificate path
+ result.pop(0)
+ return result
+
+
+def get_cert_dict(data):
+ if data is None:
+ return {}
+
+ result = data.to_dict()
+ result.pop('nonce')
+ result['signature_algorithm'] = data.signature_type
+
+ return result
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ force=dict(type='bool', default=False),
+ identifier=dict(type='str'),
+ options=dict(type='list', elements='str'),
+ path=dict(type='path', required=True),
+ pkcs11_provider=dict(type='str'),
+ principals=dict(type='list', elements='str'),
+ public_key=dict(type='path'),
+ regenerate=dict(
+ type='str',
+ default='partial_idempotence',
+ choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
+ ),
+ signature_algorithm=dict(type='str', choices=['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512']),
+ signing_key=dict(type='path'),
+ serial_number=dict(type='int'),
+ state=dict(type='str', default='present', choices=['absent', 'present']),
+ type=dict(type='str', choices=['host', 'user']),
+ use_agent=dict(type='bool', default=False),
+ valid_at=dict(type='str'),
+ valid_from=dict(type='str'),
+ valid_to=dict(type='str'),
+ ignore_timestamps=dict(type='bool', default=False),
+ ),
+ supports_check_mode=True,
+ add_file_common_args=True,
+ required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])],
+ )
+
+ Certificate(module).execute()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssh_keypair.py b/ansible_collections/community/crypto/plugins/modules/openssh_keypair.py
new file mode 100644
index 00000000..274125fc
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssh_keypair.py
@@ -0,0 +1,244 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2018, David Kainz <dkainz@mgit.at> <dave.jokain@gmx.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+DOCUMENTATION = '''
+---
+module: openssh_keypair
+author: "David Kainz (@lolcube)"
+short_description: Generate OpenSSH private and public keys
+description:
+ - "This module allows one to (re)generate OpenSSH private and public keys. It uses
+ ssh-keygen to generate keys. One can generate C(rsa), C(dsa), C(rsa1), C(ed25519)
+ or C(ecdsa) private keys."
+requirements:
+ - ssh-keygen (if I(backend=openssh))
+ - cryptography >= 2.6 (if I(backend=cryptography) and OpenSSH < 7.8 is installed)
+ - cryptography >= 3.0 (if I(backend=cryptography) and OpenSSH >= 7.8 is installed)
+extends_documentation_fragment:
+ - ansible.builtin.files
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+ safe_file_operations:
+ support: full
+options:
+ state:
+ description:
+ - Whether the private and public keys should exist or not, taking action if the state is different from what is stated.
+ type: str
+ default: present
+ choices: [ present, absent ]
+ size:
+ description:
+ - "Specifies the number of bits in the private key to create. For RSA keys, the minimum size is 1024 bits and the default is 4096 bits.
+ Generally, 2048 bits is considered sufficient. DSA keys must be exactly 1024 bits as specified by FIPS 186-2.
+ For ECDSA keys, size determines the key length by selecting from one of three elliptic curve sizes: 256, 384 or 521 bits.
+ Attempting to use bit lengths other than these three values for ECDSA keys will cause this module to fail.
+ Ed25519 keys have a fixed length and the size will be ignored."
+ type: int
+ type:
+ description:
+ - "The algorithm used to generate the SSH private key. C(rsa1) is for protocol version 1.
+ C(rsa1) is deprecated and may not be supported by every version of ssh-keygen."
+ type: str
+ default: rsa
+ choices: ['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519']
+ force:
+ description:
+ - Should the key be regenerated even if it already exists
+ type: bool
+ default: false
+ path:
+ description:
+ - Name of the files containing the public and private key. The file containing the public key will have the extension C(.pub).
+ type: path
+ required: true
+ comment:
+ description:
+ - Provides a new comment to the public key.
+ type: str
+ passphrase:
+ description:
+ - Passphrase used to decrypt an existing private key or encrypt a newly generated private key.
+ - Passphrases are not supported for I(type=rsa1).
+ - Can only be used when I(backend=cryptography), or when I(backend=auto) and a required C(cryptography) version is installed.
+ type: str
+ version_added: 1.7.0
+ private_key_format:
+ description:
+ - Used when I(backend=cryptography) to select a format for the private key at the provided I(path).
+ - When set to C(auto) this module will match the key format of the installed OpenSSH version.
+ - For OpenSSH < 7.8 private keys will be in PKCS1 format except ed25519 keys which will be in OpenSSH format.
+ - For OpenSSH >= 7.8 all private key types will be in the OpenSSH format.
+ - Using this option when I(regenerate=partial_idempotence) or I(regenerate=full_idempotence) will cause
+ a new keypair to be generated if the private key's format does not match the value of I(private_key_format).
+ This module will not however convert existing private keys between formats.
+ type: str
+ default: auto
+ choices:
+ - auto
+ - pkcs1
+ - pkcs8
+ - ssh
+ version_added: 1.7.0
+ backend:
+ description:
+ - Selects between the C(cryptography) library or the OpenSSH binary C(opensshbin).
+ - C(auto) will default to C(opensshbin) unless the OpenSSH binary is not installed or when using I(passphrase).
+ type: str
+ default: auto
+ choices:
+ - auto
+ - cryptography
+ - opensshbin
+ version_added: 1.7.0
+ regenerate:
+ description:
+ - Allows to configure in which situations the module is allowed to regenerate private keys.
+ The module will always generate a new key if the destination file does not exist.
+ - By default, the key will be regenerated when it does not match the module's options,
+ except when the key cannot be read or the passphrase does not match. Please note that
+ this B(changed) for Ansible 2.10. For Ansible 2.9, the behavior was as if C(full_idempotence)
+ is specified.
+ - If set to C(never), the module will fail if the key cannot be read or the passphrase
+ is not matching, and will never regenerate an existing key.
+ - If set to C(fail), the module will fail if the key does not correspond to the module's
+ options.
+ - If set to C(partial_idempotence), the key will be regenerated if it does not conform to
+ the module's options. The key is B(not) regenerated if it cannot be read (broken file),
+ the key is protected by an unknown passphrase, or when they key is not protected by a
+ passphrase, but a passphrase is specified.
+ - If set to C(full_idempotence), the key will be regenerated if it does not conform to the
+ module's options. This is also the case if the key cannot be read (broken file), the key
+ is protected by an unknown passphrase, or when they key is not protected by a passphrase,
+ but a passphrase is specified. Make sure you have a B(backup) when using this option!
+ - If set to C(always), the module will always regenerate the key. This is equivalent to
+ setting I(force) to C(true).
+ - Note that adjusting the comment and the permissions can be changed without regeneration.
+ Therefore, even for C(never), the task can result in changed.
+ type: str
+ choices:
+ - never
+ - fail
+ - partial_idempotence
+ - full_idempotence
+ - always
+ default: partial_idempotence
+ version_added: '1.0.0'
+notes:
+ - In case the ssh key is broken or password protected, the module will fail.
+ Set the I(force) option to C(true) if you want to regenerate the keypair.
+ - In the case a custom C(mode), C(group), C(owner), or other file attribute is provided it will be applied to both key files.
+'''
+
+EXAMPLES = '''
+- name: Generate an OpenSSH keypair with the default values (4096 bits, rsa)
+ community.crypto.openssh_keypair:
+ path: /tmp/id_ssh_rsa
+
+- name: Generate an OpenSSH keypair with the default values (4096 bits, rsa) and encrypted private key
+ community.crypto.openssh_keypair:
+ path: /tmp/id_ssh_rsa
+ passphrase: super_secret_password
+
+- name: Generate an OpenSSH rsa keypair with a different size (2048 bits)
+ community.crypto.openssh_keypair:
+ path: /tmp/id_ssh_rsa
+ size: 2048
+
+- name: Force regenerate an OpenSSH keypair if it already exists
+ community.crypto.openssh_keypair:
+ path: /tmp/id_ssh_rsa
+ force: True
+
+- name: Generate an OpenSSH keypair with a different algorithm (dsa)
+ community.crypto.openssh_keypair:
+ path: /tmp/id_ssh_dsa
+ type: dsa
+'''
+
+RETURN = '''
+size:
+ description: Size (in bits) of the SSH private key.
+ returned: changed or success
+ type: int
+ sample: 4096
+type:
+ description: Algorithm used to generate the SSH private key.
+ returned: changed or success
+ type: str
+ sample: rsa
+filename:
+ description: Path to the generated SSH private key file.
+ returned: changed or success
+ type: str
+ sample: /tmp/id_ssh_rsa
+fingerprint:
+ description: The fingerprint of the key.
+ returned: changed or success
+ type: str
+ sample: SHA256:r4YCZxihVjedH2OlfjVGI6Y5xAYtdCwk8VxKyzVyYfM
+public_key:
+ description: The public key of the generated SSH private key.
+ returned: changed or success
+ type: str
+ sample: ssh-rsa AAAAB3Nza(...omitted...)veL4E3Xcw==
+comment:
+ description: The comment of the generated key.
+ returned: changed or success
+ type: str
+ sample: test@comment
+'''
+
+from ansible.module_utils.basic import AnsibleModule
+
+from ansible_collections.community.crypto.plugins.module_utils.openssh.backends.keypair_backend import (
+ select_backend
+)
+
+
+def main():
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(type='str', default='present', choices=['present', 'absent']),
+ size=dict(type='int'),
+ type=dict(type='str', default='rsa', choices=['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519']),
+ force=dict(type='bool', default=False),
+ path=dict(type='path', required=True),
+ comment=dict(type='str'),
+ regenerate=dict(
+ type='str',
+ default='partial_idempotence',
+ choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
+ ),
+ passphrase=dict(type='str', no_log=True),
+ private_key_format=dict(
+ type='str',
+ default='auto',
+ no_log=False,
+ choices=['auto', 'pkcs1', 'pkcs8', 'ssh']),
+ backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'opensshbin'])
+ ),
+ supports_check_mode=True,
+ add_file_common_args=True,
+ )
+
+ keypair = select_backend(module, module.params['backend'])[1]
+
+ keypair.execute()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_csr.py b/ansible_collections/community/crypto/plugins/modules/openssl_csr.py
new file mode 100644
index 00000000..69b663b2
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_csr.py
@@ -0,0 +1,359 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_csr
+short_description: Generate OpenSSL Certificate Signing Request (CSR)
+description:
+ - "Please note that the module regenerates an existing CSR if it does not match the module's
+ options, or if it seems to be corrupt. If you are concerned that this could overwrite
+ your existing CSR, consider using the I(backup) option."
+author:
+ - Yanis Guenane (@Spredzy)
+ - Felix Fontein (@felixfontein)
+extends_documentation_fragment:
+ - ansible.builtin.files
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+ - community.crypto.module_csr
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+ safe_file_operations:
+ support: full
+options:
+ state:
+ description:
+ - Whether the certificate signing request should exist or not, taking action if the state is different from what is stated.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ force:
+ description:
+ - Should the certificate signing request be forced regenerated by this ansible module.
+ type: bool
+ default: false
+ path:
+ description:
+ - The name of the file into which the generated OpenSSL certificate signing request will be written.
+ type: path
+ required: true
+ backup:
+ description:
+ - Create a backup file including a timestamp so you can get the original
+ CSR back if you overwrote it with a new one by accident.
+ type: bool
+ default: false
+ return_content:
+ description:
+ - If set to C(true), will return the (current or generated) CSR's content as I(csr).
+ type: bool
+ default: false
+ version_added: "1.0.0"
+ privatekey_content:
+ version_added: "1.0.0"
+ name_constraints_permitted:
+ version_added: 1.1.0
+ name_constraints_excluded:
+ version_added: 1.1.0
+ name_constraints_critical:
+ version_added: 1.1.0
+seealso:
+ - module: community.crypto.openssl_csr_pipe
+'''
+
+EXAMPLES = r'''
+- name: Generate an OpenSSL Certificate Signing Request
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ common_name: www.ansible.com
+
+- name: Generate an OpenSSL Certificate Signing Request with an inline key
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ privatekey_content: "{{ private_key_content }}"
+ common_name: www.ansible.com
+
+- name: Generate an OpenSSL Certificate Signing Request with a passphrase protected private key
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ privatekey_passphrase: ansible
+ common_name: www.ansible.com
+
+- name: Generate an OpenSSL Certificate Signing Request with Subject information
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ country_name: FR
+ organization_name: Ansible
+ email_address: jdoe@ansible.com
+ common_name: www.ansible.com
+
+- name: Generate an OpenSSL Certificate Signing Request with subjectAltName extension
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ subject_alt_name: 'DNS:www.ansible.com,DNS:m.ansible.com'
+
+- name: Generate an OpenSSL CSR with subjectAltName extension with dynamic list
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ subject_alt_name: "{{ item.value | map('regex_replace', '^', 'DNS:') | list }}"
+ with_dict:
+ dns_server:
+ - www.ansible.com
+ - m.ansible.com
+
+- name: Force regenerate an OpenSSL Certificate Signing Request
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ force: true
+ common_name: www.ansible.com
+
+- name: Generate an OpenSSL Certificate Signing Request with special key usages
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ common_name: www.ansible.com
+ key_usage:
+ - digitalSignature
+ - keyAgreement
+ extended_key_usage:
+ - clientAuth
+
+- name: Generate an OpenSSL Certificate Signing Request with OCSP Must Staple
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ common_name: www.ansible.com
+ ocsp_must_staple: true
+
+- name: Generate an OpenSSL Certificate Signing Request for WinRM Certificate authentication
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/winrm.auth.csr
+ privatekey_path: /etc/ssl/private/winrm.auth.pem
+ common_name: username
+ extended_key_usage:
+ - clientAuth
+ subject_alt_name: otherName:1.3.6.1.4.1.311.20.2.3;UTF8:username@localhost
+
+- name: Generate an OpenSSL Certificate Signing Request with a CRL distribution point
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ common_name: www.ansible.com
+ crl_distribution_points:
+ - full_name:
+ - "URI:https://ca.example.com/revocations.crl"
+ crl_issuer:
+ - "URI:https://ca.example.com/"
+ reasons:
+ - key_compromise
+ - ca_compromise
+ - cessation_of_operation
+'''
+
+RETURN = r'''
+privatekey:
+ description:
+ - Path to the TLS/SSL private key the CSR was generated for
+ - Will be C(none) if the private key has been provided in I(privatekey_content).
+ returned: changed or success
+ type: str
+ sample: /etc/ssl/private/ansible.com.pem
+filename:
+ description: Path to the generated Certificate Signing Request
+ returned: changed or success
+ type: str
+ sample: /etc/ssl/csr/www.ansible.com.csr
+subject:
+ description: A list of the subject tuples attached to the CSR
+ returned: changed or success
+ type: list
+ elements: list
+ sample: [['CN', 'www.ansible.com'], ['O', 'Ansible']]
+subjectAltName:
+ description: The alternative names this CSR is valid for
+ returned: changed or success
+ type: list
+ elements: str
+ sample: [ 'DNS:www.ansible.com', 'DNS:m.ansible.com' ]
+keyUsage:
+ description: Purpose for which the public key may be used
+ returned: changed or success
+ type: list
+ elements: str
+ sample: [ 'digitalSignature', 'keyAgreement' ]
+extendedKeyUsage:
+ description: Additional restriction on the public key purposes
+ returned: changed or success
+ type: list
+ elements: str
+ sample: [ 'clientAuth' ]
+basicConstraints:
+ description: Indicates if the certificate belongs to a CA
+ returned: changed or success
+ type: list
+ elements: str
+ sample: ['CA:TRUE', 'pathLenConstraint:0']
+ocsp_must_staple:
+ description: Indicates whether the certificate has the OCSP
+ Must Staple feature enabled
+ returned: changed or success
+ type: bool
+ sample: false
+name_constraints_permitted:
+ description: List of permitted subtrees to sign certificates for.
+ returned: changed or success
+ type: list
+ elements: str
+ sample: ['email:.somedomain.com']
+ version_added: 1.1.0
+name_constraints_excluded:
+ description: List of excluded subtrees the CA cannot sign certificates for.
+ returned: changed or success
+ type: list
+ elements: str
+ sample: ['email:.com']
+ version_added: 1.1.0
+backup_file:
+ description: Name of backup file created.
+ returned: changed and if I(backup) is C(true)
+ type: str
+ sample: /path/to/www.ansible.com.csr.2019-03-09@11:22~
+csr:
+ description: The (current or generated) CSR's content.
+ returned: if I(state) is C(present) and I(return_content) is C(true)
+ type: str
+ version_added: "1.0.0"
+'''
+
+import os
+
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr import (
+ select_backend,
+ get_csr_argument_spec,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.io import (
+ load_file_if_exists,
+ write_file,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ OpenSSLObject,
+)
+
+
+class CertificateSigningRequestModule(OpenSSLObject):
+
+ def __init__(self, module, module_backend):
+ super(CertificateSigningRequestModule, self).__init__(
+ module.params['path'],
+ module.params['state'],
+ module.params['force'],
+ module.check_mode
+ )
+ self.module_backend = module_backend
+ self.return_content = module.params['return_content']
+
+ self.backup = module.params['backup']
+ self.backup_file = None
+
+ self.module_backend.set_existing(load_file_if_exists(self.path, module))
+
+ def generate(self, module):
+ '''Generate the certificate signing request.'''
+ if self.force or self.module_backend.needs_regeneration():
+ if not self.check_mode:
+ self.module_backend.generate_csr()
+ result = self.module_backend.get_csr_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.check_file_absent_if_check_mode(file_args['path']):
+ self.changed = True
+ else:
+ self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
+
+ def remove(self, module):
+ self.module_backend.set_existing(None)
+ if self.backup and not self.check_mode:
+ self.backup_file = module.backup_local(self.path)
+ super(CertificateSigningRequestModule, self).remove(module)
+
+ def dump(self):
+ '''Serialize the object into a dictionary.'''
+ result = self.module_backend.dump(include_csr=self.return_content)
+ result.update({
+ 'filename': self.path,
+ 'changed': self.changed,
+ })
+ if self.backup_file:
+ result['backup_file'] = self.backup_file
+ return result
+
+
+def main():
+ argument_spec = get_csr_argument_spec()
+ argument_spec.argument_spec.update(dict(
+ state=dict(type='str', default='present', choices=['absent', 'present']),
+ force=dict(type='bool', default=False),
+ path=dict(type='path', required=True),
+ backup=dict(type='bool', default=False),
+ return_content=dict(type='bool', default=False),
+ ))
+ argument_spec.required_if.extend([('state', 'present', rof, True) for rof in argument_spec.required_one_of])
+ argument_spec.required_one_of = []
+ module = argument_spec.create_ansible_module(
+ add_file_common_args=True,
+ supports_check_mode=True,
+ )
+
+ base_dir = os.path.dirname(module.params['path']) or '.'
+ if not os.path.isdir(base_dir):
+ module.fail_json(name=base_dir, msg='The directory %s does not exist or the file is not a directory' % base_dir)
+
+ try:
+ backend = module.params['select_crypto_backend']
+ backend, module_backend = select_backend(module, backend)
+
+ csr = CertificateSigningRequestModule(module, module_backend)
+ if module.params['state'] == 'present':
+ csr.generate(module)
+ else:
+ csr.remove(module)
+
+ result = csr.dump()
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_csr_info.py b/ansible_collections/community/crypto/plugins/modules/openssl_csr_info.py
new file mode 100644
index 00000000..7ed0b1c4
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_csr_info.py
@@ -0,0 +1,359 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_csr_info
+short_description: Provide information of OpenSSL Certificate Signing Requests (CSR)
+description:
+ - This module allows one to query information on OpenSSL Certificate Signing Requests (CSR).
+ - In case the CSR signature cannot be validated, the module will fail. In this case, all return
+ variables are still returned.
+ - It uses the cryptography python library to interact with OpenSSL.
+requirements:
+ - cryptography >= 1.3
+author:
+ - Felix Fontein (@felixfontein)
+ - Yanis Guenane (@Spredzy)
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - community.crypto.attributes.info_module
+ - community.crypto.name_encoding
+options:
+ path:
+ description:
+ - Remote absolute path where the CSR file is loaded from.
+ - Either I(path) or I(content) must be specified, but not both.
+ type: path
+ content:
+ description:
+ - Content of the CSR file.
+ - Either I(path) or I(content) must be specified, but not both.
+ type: str
+ version_added: "1.0.0"
+ select_crypto_backend:
+ description:
+ - Determines which crypto backend to use.
+ - The default choice is C(auto), which tries to use C(cryptography) if available.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography ]
+
+seealso:
+ - module: community.crypto.openssl_csr
+ - module: community.crypto.openssl_csr_pipe
+ - ref: community.crypto.openssl_csr_info filter <ansible_collections.community.crypto.openssl_csr_info_filter>
+ # - plugin: community.crypto.openssl_csr_info
+ # plugin_type: filter
+ description: A filter variant of this module.
+'''
+
+EXAMPLES = r'''
+- name: Generate an OpenSSL Certificate Signing Request
+ community.crypto.openssl_csr:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ common_name: www.ansible.com
+
+- name: Get information on the CSR
+ community.crypto.openssl_csr_info:
+ path: /etc/ssl/csr/www.ansible.com.csr
+ register: result
+
+- name: Dump information
+ debug:
+ var: result
+'''
+
+RETURN = r'''
+signature_valid:
+ description:
+ - Whether the CSR's signature is valid.
+ - In case the check returns C(false), the module will fail.
+ returned: success
+ type: bool
+basic_constraints:
+ description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present.
+ returned: success
+ type: list
+ elements: str
+ sample: ['CA:TRUE', 'pathlen:1']
+basic_constraints_critical:
+ description: Whether the C(basic_constraints) extension is critical.
+ returned: success
+ type: bool
+extended_key_usage:
+ description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present.
+ returned: success
+ type: list
+ elements: str
+ sample: [Biometric Info, DVCS, Time Stamping]
+extended_key_usage_critical:
+ description: Whether the C(extended_key_usage) extension is critical.
+ returned: success
+ type: bool
+extensions_by_oid:
+ description: Returns a dictionary for every extension OID
+ returned: success
+ type: dict
+ contains:
+ critical:
+ description: Whether the extension is critical.
+ returned: success
+ type: bool
+ value:
+ description:
+ - The Base64 encoded value (in DER format) of the extension.
+ - B(Note) that depending on the C(cryptography) version used, it is
+ not possible to extract the ASN.1 content of the extension, but only
+ to provide the re-encoded content of the extension in case it was
+ parsed by C(cryptography). This should usually result in exactly the
+ same value, except if the original extension value was malformed.
+ returned: success
+ type: str
+ sample: "MAMCAQU="
+ sample: {"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}
+key_usage:
+ description: Entries in the C(key_usage) extension, or C(none) if extension is not present.
+ returned: success
+ type: str
+ sample: [Key Agreement, Data Encipherment]
+key_usage_critical:
+ description: Whether the C(key_usage) extension is critical.
+ returned: success
+ type: bool
+subject_alt_name:
+ description:
+ - Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: list
+ elements: str
+ sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
+subject_alt_name_critical:
+ description: Whether the C(subject_alt_name) extension is critical.
+ returned: success
+ type: bool
+ocsp_must_staple:
+ description: C(true) if the OCSP Must Staple extension is present, C(none) otherwise.
+ returned: success
+ type: bool
+ocsp_must_staple_critical:
+ description: Whether the C(ocsp_must_staple) extension is critical.
+ returned: success
+ type: bool
+name_constraints_permitted:
+ description: List of permitted subtrees to sign certificates for.
+ returned: success
+ type: list
+ elements: str
+ sample: ['email:.somedomain.com']
+ version_added: 1.1.0
+name_constraints_excluded:
+ description:
+ - List of excluded subtrees the CA cannot sign certificates for.
+ - Is C(none) if extension is not present.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: list
+ elements: str
+ sample: ['email:.com']
+ version_added: 1.1.0
+name_constraints_critical:
+ description:
+ - Whether the C(name_constraints) extension is critical.
+ - Is C(none) if extension is not present.
+ returned: success
+ type: bool
+ version_added: 1.1.0
+subject:
+ description:
+ - The CSR's subject as a dictionary.
+ - Note that for repeated values, only the last one will be returned.
+ returned: success
+ type: dict
+ sample: {"commonName": "www.example.com", "emailAddress": "test@example.com"}
+subject_ordered:
+ description: The CSR's subject as an ordered list of tuples.
+ returned: success
+ type: list
+ elements: list
+ sample: [["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]
+public_key:
+ description: CSR's public key in PEM format
+ returned: success
+ type: str
+ sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
+public_key_type:
+ description:
+ - The CSR's public key's type.
+ - One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
+ - Will start with C(unknown) if the key type cannot be determined.
+ returned: success
+ type: str
+ version_added: 1.7.0
+ sample: RSA
+public_key_data:
+ description:
+ - Public key data. Depends on the public key's type.
+ returned: success
+ type: dict
+ version_added: 1.7.0
+ contains:
+ size:
+ description:
+ - Bit size of modulus (RSA) or prime number (DSA).
+ type: int
+ returned: When C(public_key_type=RSA) or C(public_key_type=DSA)
+ modulus:
+ description:
+ - The RSA key's modulus.
+ type: int
+ returned: When C(public_key_type=RSA)
+ exponent:
+ description:
+ - The RSA key's public exponent.
+ type: int
+ returned: When C(public_key_type=RSA)
+ p:
+ description:
+ - The C(p) value for DSA.
+ - This is the prime modulus upon which arithmetic takes place.
+ type: int
+ returned: When C(public_key_type=DSA)
+ q:
+ description:
+ - The C(q) value for DSA.
+ - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
+ multiplicative group of the prime field used.
+ type: int
+ returned: When C(public_key_type=DSA)
+ g:
+ description:
+ - The C(g) value for DSA.
+ - This is the element spanning the subgroup of the multiplicative group of the prime field used.
+ type: int
+ returned: When C(public_key_type=DSA)
+ curve:
+ description:
+ - The curve's name for ECC.
+ type: str
+ returned: When C(public_key_type=ECC)
+ exponent_size:
+ description:
+ - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
+ type: int
+ returned: When C(public_key_type=ECC)
+ x:
+ description:
+ - The C(x) coordinate for the public point on the elliptic curve.
+ type: int
+ returned: When C(public_key_type=ECC)
+ y:
+ description:
+ - For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
+ - For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
+ type: int
+ returned: When C(public_key_type=DSA) or C(public_key_type=ECC)
+public_key_fingerprints:
+ description:
+ - Fingerprints of CSR's public key.
+ - For every hash algorithm available, the fingerprint is computed.
+ returned: success
+ type: dict
+ sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
+ 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
+subject_key_identifier:
+ description:
+ - The CSR's subject key identifier.
+ - The identifier is returned in hexadecimal, with C(:) used to separate bytes.
+ - Is C(none) if the C(SubjectKeyIdentifier) extension is not present.
+ returned: success
+ type: str
+ sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
+authority_key_identifier:
+ description:
+ - The CSR's authority key identifier.
+ - The identifier is returned in hexadecimal, with C(:) used to separate bytes.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ returned: success
+ type: str
+ sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
+authority_cert_issuer:
+ description:
+ - The CSR's authority cert issuer as a list of general names.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: list
+ elements: str
+ sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
+authority_cert_serial_number:
+ description:
+ - The CSR's authority cert serial number.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ returned: success
+ type: int
+ sample: 12345
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr_info import (
+ select_backend,
+)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ path=dict(type='path'),
+ content=dict(type='str'),
+ name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
+ select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
+ ),
+ required_one_of=(
+ ['path', 'content'],
+ ),
+ mutually_exclusive=(
+ ['path', 'content'],
+ ),
+ supports_check_mode=True,
+ )
+
+ if module.params['content'] is not None:
+ data = module.params['content'].encode('utf-8')
+ else:
+ try:
+ with open(module.params['path'], 'rb') as f:
+ data = f.read()
+ except (IOError, OSError) as e:
+ module.fail_json(msg='Error while reading CSR file from disk: {0}'.format(e))
+
+ backend, module_backend = select_backend(module, module.params['select_crypto_backend'], data, validate_signature=True)
+
+ try:
+ result = module_backend.get_info()
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_csr_pipe.py b/ansible_collections/community/crypto/plugins/modules/openssl_csr_pipe.py
new file mode 100644
index 00000000..01a3fd79
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_csr_pipe.py
@@ -0,0 +1,183 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_csr_pipe
+short_description: Generate OpenSSL Certificate Signing Request (CSR)
+version_added: 1.3.0
+description:
+ - "Please note that the module regenerates an existing CSR if it does not match the module's
+ options, or if it seems to be corrupt."
+author:
+ - Yanis Guenane (@Spredzy)
+ - Felix Fontein (@felixfontein)
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - community.crypto.module_csr
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+options:
+ content:
+ description:
+ - The existing CSR.
+ type: str
+seealso:
+- module: community.crypto.openssl_csr
+'''
+
+EXAMPLES = r'''
+- name: Generate an OpenSSL Certificate Signing Request
+ community.crypto.openssl_csr_pipe:
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ common_name: www.ansible.com
+ register: result
+- debug:
+ var: result.csr
+
+- 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 }}"
+ common_name: www.ansible.com
+ register: result
+- name: Store CSR
+ ansible.builtin.copy:
+ dest: /etc/ssl/csr/www.ansible.com.csr
+ content: "{{ result.csr }}"
+ when: result is changed
+'''
+
+RETURN = r'''
+privatekey:
+ description:
+ - Path to the TLS/SSL private key the CSR was generated for
+ - Will be C(none) if the private key has been provided in I(privatekey_content).
+ returned: changed or success
+ type: str
+ sample: /etc/ssl/private/ansible.com.pem
+subject:
+ description: A list of the subject tuples attached to the CSR
+ returned: changed or success
+ type: list
+ elements: list
+ sample: [['CN', 'www.ansible.com'], ['O', 'Ansible']]
+subjectAltName:
+ description: The alternative names this CSR is valid for
+ returned: changed or success
+ type: list
+ elements: str
+ sample: [ 'DNS:www.ansible.com', 'DNS:m.ansible.com' ]
+keyUsage:
+ description: Purpose for which the public key may be used
+ returned: changed or success
+ type: list
+ elements: str
+ sample: [ 'digitalSignature', 'keyAgreement' ]
+extendedKeyUsage:
+ description: Additional restriction on the public key purposes
+ returned: changed or success
+ type: list
+ elements: str
+ sample: [ 'clientAuth' ]
+basicConstraints:
+ description: Indicates if the certificate belongs to a CA
+ returned: changed or success
+ type: list
+ elements: str
+ sample: ['CA:TRUE', 'pathLenConstraint:0']
+ocsp_must_staple:
+ description: Indicates whether the certificate has the OCSP
+ Must Staple feature enabled
+ returned: changed or success
+ type: bool
+ sample: false
+name_constraints_permitted:
+ description: List of permitted subtrees to sign certificates for.
+ returned: changed or success
+ type: list
+ elements: str
+ sample: ['email:.somedomain.com']
+name_constraints_excluded:
+ description: List of excluded subtrees the CA cannot sign certificates for.
+ returned: changed or success
+ type: list
+ elements: str
+ sample: ['email:.com']
+csr:
+ description: The (current or generated) CSR's content.
+ returned: changed or success
+ type: str
+'''
+
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.csr import (
+ select_backend,
+ get_csr_argument_spec,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+
+class CertificateSigningRequestModule(object):
+ 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):
+ '''Generate the certificate signing request.'''
+ if self.module_backend.needs_regeneration():
+ if not self.check_mode:
+ self.module_backend.generate_csr()
+ self.changed = True
+
+ def dump(self):
+ '''Serialize the object into a dictionary.'''
+ result = self.module_backend.dump(include_csr=True)
+ result.update({
+ 'changed': self.changed,
+ })
+ return result
+
+
+def main():
+ argument_spec = get_csr_argument_spec()
+ argument_spec.argument_spec.update(dict(
+ content=dict(type='str'),
+ ))
+ module = argument_spec.create_ansible_module(
+ supports_check_mode=True,
+ )
+
+ try:
+ backend = module.params['select_crypto_backend']
+ backend, module_backend = select_backend(module, backend)
+
+ csr = CertificateSigningRequestModule(module, module_backend)
+ csr.generate(module)
+ result = csr.dump()
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_dhparam.py b/ansible_collections/community/crypto/plugins/modules/openssl_dhparam.py
new file mode 100644
index 00000000..d9e1e982
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_dhparam.py
@@ -0,0 +1,431 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017, Thom Wiggers <ansible@thomwiggers.nl>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_dhparam
+short_description: Generate OpenSSL Diffie-Hellman Parameters
+description:
+ - This module allows one to (re)generate OpenSSL DH-params.
+ - This module uses file common arguments to specify generated file permissions.
+ - "Please note that the module regenerates existing DH params if they do not
+ match the module's options. If you are concerned that this could overwrite
+ your existing DH params, consider using the I(backup) option."
+ - The module can use the cryptography Python library, or the C(openssl) executable.
+ By default, it tries to detect which one is available. This can be overridden
+ with the I(select_crypto_backend) option.
+requirements:
+ - Either cryptography >= 2.0
+ - Or OpenSSL binary C(openssl)
+author:
+ - Thom Wiggers (@thomwiggers)
+extends_documentation_fragment:
+ - ansible.builtin.files
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+ safe_file_operations:
+ support: full
+options:
+ state:
+ description:
+ - Whether the parameters should exist or not,
+ taking action if the state is different from what is stated.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ size:
+ description:
+ - Size (in bits) of the generated DH-params.
+ type: int
+ default: 4096
+ force:
+ description:
+ - Should the parameters be regenerated even it it already exists.
+ type: bool
+ default: false
+ path:
+ description:
+ - Name of the file in which the generated parameters will be saved.
+ type: path
+ required: true
+ backup:
+ description:
+ - Create a backup file including a timestamp so you can get the original
+ DH params back if you overwrote them with new ones by accident.
+ type: bool
+ default: false
+ select_crypto_backend:
+ description:
+ - Determines which crypto backend to use.
+ - The default choice is C(auto), which tries to use C(cryptography) if available, and falls back to C(openssl).
+ - If set to C(openssl), will try to use the OpenSSL C(openssl) executable.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography, openssl ]
+ version_added: "1.0.0"
+ return_content:
+ description:
+ - If set to C(true), will return the (current or generated) DH parameter's content as I(dhparams).
+ type: bool
+ default: false
+ version_added: "1.0.0"
+seealso:
+ - module: community.crypto.x509_certificate
+ - module: community.crypto.openssl_csr
+ - module: community.crypto.openssl_pkcs12
+ - module: community.crypto.openssl_privatekey
+ - module: community.crypto.openssl_publickey
+'''
+
+EXAMPLES = r'''
+- name: Generate Diffie-Hellman parameters with the default size (4096 bits)
+ community.crypto.openssl_dhparam:
+ path: /etc/ssl/dhparams.pem
+
+- name: Generate DH Parameters with a different size (2048 bits)
+ community.crypto.openssl_dhparam:
+ path: /etc/ssl/dhparams.pem
+ size: 2048
+
+- name: Force regenerate an DH parameters if they already exist
+ community.crypto.openssl_dhparam:
+ path: /etc/ssl/dhparams.pem
+ force: true
+'''
+
+RETURN = r'''
+size:
+ description: Size (in bits) of the Diffie-Hellman parameters.
+ returned: changed or success
+ type: int
+ sample: 4096
+filename:
+ description: Path to the generated Diffie-Hellman parameters.
+ returned: changed or success
+ type: str
+ sample: /etc/ssl/dhparams.pem
+backup_file:
+ description: Name of backup file created.
+ returned: changed and if I(backup) is C(true)
+ type: str
+ sample: /path/to/dhparams.pem.2019-03-09@11:22~
+dhparams:
+ description: The (current or generated) DH params' content.
+ returned: if I(state) is C(present) and I(return_content) is C(true)
+ type: str
+ version_added: "1.0.0"
+'''
+
+import abc
+import os
+import re
+import tempfile
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.io import (
+ load_file_if_exists,
+ write_file,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.math import (
+ count_bits,
+)
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '2.0'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ import cryptography.exceptions
+ import cryptography.hazmat.backends
+ import cryptography.hazmat.primitives.asymmetric.dh
+ import cryptography.hazmat.primitives.serialization
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+
+class DHParameterError(Exception):
+ pass
+
+
+class DHParameterBase(object):
+
+ def __init__(self, module):
+ self.state = module.params['state']
+ self.path = module.params['path']
+ self.size = module.params['size']
+ self.force = module.params['force']
+ self.changed = False
+ self.return_content = module.params['return_content']
+
+ self.backup = module.params['backup']
+ self.backup_file = None
+
+ @abc.abstractmethod
+ def _do_generate(self, module):
+ """Actually generate the DH params."""
+ pass
+
+ def generate(self, module):
+ """Generate DH params."""
+ changed = False
+
+ # ony generate when necessary
+ if self.force or not self._check_params_valid(module):
+ self._do_generate(module)
+ changed = True
+
+ # fix permissions (checking force not necessary as done above)
+ if not self._check_fs_attributes(module):
+ # Fix done implicitly by
+ # AnsibleModule.set_fs_attributes_if_different
+ changed = True
+
+ self.changed = changed
+
+ def remove(self, module):
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ try:
+ os.remove(self.path)
+ self.changed = True
+ except OSError as exc:
+ module.fail_json(msg=to_native(exc))
+
+ def check(self, module):
+ """Ensure the resource is in its desired state."""
+ if self.force:
+ return False
+ return self._check_params_valid(module) and self._check_fs_attributes(module)
+
+ @abc.abstractmethod
+ def _check_params_valid(self, module):
+ """Check if the params are in the correct state"""
+ pass
+
+ def _check_fs_attributes(self, module):
+ """Checks (and changes if not in check mode!) fs attributes"""
+ file_args = module.load_file_common_arguments(module.params)
+ if module.check_file_absent_if_check_mode(file_args['path']):
+ return False
+ return not module.set_fs_attributes_if_different(file_args, False)
+
+ def dump(self):
+ """Serialize the object into a dictionary."""
+
+ result = {
+ 'size': self.size,
+ 'filename': self.path,
+ 'changed': self.changed,
+ }
+ if self.backup_file:
+ result['backup_file'] = self.backup_file
+ if self.return_content:
+ content = load_file_if_exists(self.path, ignore_errors=True)
+ result['dhparams'] = content.decode('utf-8') if content else None
+
+ return result
+
+
+class DHParameterAbsent(DHParameterBase):
+
+ def __init__(self, module):
+ super(DHParameterAbsent, self).__init__(module)
+
+ def _do_generate(self, module):
+ """Actually generate the DH params."""
+ pass
+
+ def _check_params_valid(self, module):
+ """Check if the params are in the correct state"""
+ pass
+
+
+class DHParameterOpenSSL(DHParameterBase):
+
+ def __init__(self, module):
+ super(DHParameterOpenSSL, self).__init__(module)
+ self.openssl_bin = module.get_bin_path('openssl', True)
+
+ def _do_generate(self, module):
+ """Actually generate the DH params."""
+ # create a tempfile
+ fd, tmpsrc = tempfile.mkstemp()
+ os.close(fd)
+ module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
+ # openssl dhparam -out <path> <bits>
+ command = [self.openssl_bin, 'dhparam', '-out', tmpsrc, str(self.size)]
+ rc, dummy, err = module.run_command(command, check_rc=False)
+ if rc != 0:
+ raise DHParameterError(to_native(err))
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ try:
+ module.atomic_move(tmpsrc, self.path)
+ except Exception as e:
+ module.fail_json(msg="Failed to write to file %s: %s" % (self.path, str(e)))
+
+ def _check_params_valid(self, module):
+ """Check if the params are in the correct state"""
+ command = [self.openssl_bin, 'dhparam', '-check', '-text', '-noout', '-in', self.path]
+ rc, out, err = module.run_command(command, check_rc=False)
+ result = to_native(out)
+ if rc != 0:
+ # If the call failed the file probably does not exist or is
+ # unreadable
+ return False
+ # output contains "(xxxx bit)"
+ match = re.search(r"Parameters:\s+\((\d+) bit\).*", result)
+ if not match:
+ return False # No "xxxx bit" in output
+
+ bits = int(match.group(1))
+
+ # if output contains "WARNING" we've got a problem
+ if "WARNING" in result or "WARNING" in to_native(err):
+ return False
+
+ return bits == self.size
+
+
+class DHParameterCryptography(DHParameterBase):
+
+ def __init__(self, module):
+ super(DHParameterCryptography, self).__init__(module)
+ self.crypto_backend = cryptography.hazmat.backends.default_backend()
+
+ def _do_generate(self, module):
+ """Actually generate the DH params."""
+ # Generate parameters
+ params = cryptography.hazmat.primitives.asymmetric.dh.generate_parameters(
+ generator=2,
+ key_size=self.size,
+ backend=self.crypto_backend,
+ )
+ # Serialize parameters
+ result = params.parameter_bytes(
+ encoding=cryptography.hazmat.primitives.serialization.Encoding.PEM,
+ format=cryptography.hazmat.primitives.serialization.ParameterFormat.PKCS3,
+ )
+ # Write result
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ write_file(module, result)
+
+ def _check_params_valid(self, module):
+ """Check if the params are in the correct state"""
+ # Load parameters
+ try:
+ with open(self.path, 'rb') as f:
+ data = f.read()
+ params = self.crypto_backend.load_pem_parameters(data)
+ except Exception as dummy:
+ return False
+ # Check parameters
+ bits = count_bits(params.parameter_numbers().p)
+ return bits == self.size
+
+
+def main():
+ """Main function"""
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(type='str', default='present', choices=['absent', 'present']),
+ size=dict(type='int', default=4096),
+ force=dict(type='bool', default=False),
+ path=dict(type='path', required=True),
+ backup=dict(type='bool', default=False),
+ select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'openssl']),
+ return_content=dict(type='bool', default=False),
+ ),
+ supports_check_mode=True,
+ add_file_common_args=True,
+ )
+
+ base_dir = os.path.dirname(module.params['path']) or '.'
+ if not os.path.isdir(base_dir):
+ module.fail_json(
+ name=base_dir,
+ msg="The directory '%s' does not exist or the file is not a directory" % base_dir
+ )
+
+ if module.params['state'] == 'present':
+ backend = module.params['select_crypto_backend']
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
+ can_use_openssl = module.get_bin_path('openssl', False) is not None
+
+ # First try cryptography, then OpenSSL
+ if can_use_cryptography:
+ backend = 'cryptography'
+ elif can_use_openssl:
+ backend = 'openssl'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect either the required Python library cryptography (>= {0}) "
+ "or the OpenSSL binary openssl").format(MINIMAL_CRYPTOGRAPHY_VERSION))
+
+ if backend == 'openssl':
+ dhparam = DHParameterOpenSSL(module)
+ elif backend == 'cryptography':
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+ dhparam = DHParameterCryptography(module)
+
+ if module.check_mode:
+ result = dhparam.dump()
+ result['changed'] = module.params['force'] or not dhparam.check(module)
+ module.exit_json(**result)
+
+ try:
+ dhparam.generate(module)
+ except DHParameterError as exc:
+ module.fail_json(msg=to_native(exc))
+ else:
+ dhparam = DHParameterAbsent(module)
+
+ if module.check_mode:
+ result = dhparam.dump()
+ result['changed'] = os.path.exists(module.params['path'])
+ module.exit_json(**result)
+
+ if os.path.exists(module.params['path']):
+ try:
+ dhparam.remove(module)
+ except Exception as exc:
+ module.fail_json(msg=to_native(exc))
+
+ result = dhparam.dump()
+
+ module.exit_json(**result)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_pkcs12.py b/ansible_collections/community/crypto/plugins/modules/openssl_pkcs12.py
new file mode 100644
index 00000000..e74553b5
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_pkcs12.py
@@ -0,0 +1,848 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2017, Guillaume Delpierre <gde@llew.me>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_pkcs12
+author:
+- Guillaume Delpierre (@gdelpierre)
+short_description: Generate OpenSSL PKCS#12 archive
+description:
+ - This module allows one to (re-)generate PKCS#12.
+ - The module can use the cryptography Python library, or the pyOpenSSL Python
+ library. By default, it tries to detect which one is available, assuming none of the
+ I(iter_size) and I(maciter_size) options are used. This can be overridden with the
+ I(select_crypto_backend) option.
+ # Please note that the C(pyopenssl) backend has been deprecated in community.crypto x.y.0,
+ # and will be removed in community.crypto (x+1).0.0.
+requirements:
+ - PyOpenSSL >= 0.15 or cryptography >= 3.0
+extends_documentation_fragment:
+ - ansible.builtin.files
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+ safe_file_operations:
+ support: full
+options:
+ action:
+ description:
+ - C(export) or C(parse) a PKCS#12.
+ type: str
+ default: export
+ choices: [ export, parse ]
+ other_certificates:
+ description:
+ - List of other certificates to include. Pre Ansible 2.8 this parameter was called I(ca_certificates).
+ - Assumes there is one PEM-encoded certificate per file. If a file contains multiple PEM certificates,
+ set I(other_certificates_parse_all) to C(true).
+ type: list
+ elements: path
+ aliases: [ ca_certificates ]
+ other_certificates_parse_all:
+ description:
+ - If set to C(true), assumes that the files mentioned in I(other_certificates) can contain more than one
+ certificate per file (or even none per file).
+ type: bool
+ default: false
+ version_added: 1.4.0
+ certificate_path:
+ description:
+ - The path to read certificates and private keys from.
+ - Must be in PEM format.
+ type: path
+ force:
+ description:
+ - Should the file be regenerated even if it already exists.
+ type: bool
+ default: false
+ friendly_name:
+ description:
+ - Specifies the friendly name for the certificate and private key.
+ type: str
+ aliases: [ name ]
+ iter_size:
+ description:
+ - Number of times to repeat the encryption step.
+ - This is B(not considered during idempotency checks).
+ - This is only used by the C(pyopenssl) backend, or when I(encryption_level=compatibility2022).
+ - When using it, the default is C(2048) for C(pyopenssl) and C(50000) for C(cryptography).
+ type: int
+ maciter_size:
+ description:
+ - Number of times to repeat the MAC step.
+ - This is B(not considered during idempotency checks).
+ - This is only used by the C(pyopenssl) backend. When using it, the default is C(1).
+ type: int
+ encryption_level:
+ description:
+ - Determines the encryption level used.
+ - C(auto) uses the default of the selected backend. For C(cryptography), this is what the
+ cryptography library's specific version considers the best available encryption.
+ - C(compatibility2022) uses compatibility settings for older software in 2022.
+ This is only supported by the C(cryptography) backend if cryptography >= 38.0.0 is available.
+ - B(Note) that this option is B(not used for idempotency).
+ choices:
+ - auto
+ - compatibility2022
+ default: auto
+ type: str
+ version_added: 2.8.0
+ passphrase:
+ description:
+ - The PKCS#12 password.
+ - "B(Note:) PKCS12 encryption is not secure and should not be used as a security mechanism.
+ If you need to store or send a PKCS12 file safely, you should additionally encrypt it
+ with something else."
+ type: str
+ path:
+ description:
+ - Filename to write the PKCS#12 file to.
+ type: path
+ required: true
+ privatekey_passphrase:
+ description:
+ - Passphrase source to decrypt any input private keys with.
+ type: str
+ privatekey_path:
+ description:
+ - File to read private key from.
+ - Mutually exclusive with I(privatekey_content).
+ type: path
+ privatekey_content:
+ description:
+ - Content of the private key file.
+ - Mutually exclusive with I(privatekey_path).
+ type: str
+ version_added: "2.3.0"
+ state:
+ description:
+ - Whether the file should exist or not.
+ All parameters except C(path) are ignored when state is C(absent).
+ choices: [ absent, present ]
+ default: present
+ type: str
+ src:
+ description:
+ - PKCS#12 file path to parse.
+ type: path
+ backup:
+ description:
+ - Create a backup file including a timestamp so you can get the original
+ output file back if you overwrote it with a new one by accident.
+ type: bool
+ default: false
+ return_content:
+ description:
+ - If set to C(true), will return the (current or generated) PKCS#12's content as I(pkcs12).
+ type: bool
+ default: false
+ version_added: "1.0.0"
+ 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 I(iter_size) is used together with I(encryption_level != compatibility2022), or if I(maciter_size) is used,
+ C(auto) will always result in C(pyopenssl) to be chosen for backwards compatibility.
+ - 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 community.crypto x.y.0, and will be
+ # removed in community.crypto (x+1).0.0.
+ # From that point on, only the C(cryptography) backend will be available.
+ type: str
+ default: auto
+ choices: [ auto, cryptography, pyopenssl ]
+ version_added: 1.7.0
+seealso:
+ - module: community.crypto.x509_certificate
+ - module: community.crypto.openssl_csr
+ - module: community.crypto.openssl_dhparam
+ - module: community.crypto.openssl_privatekey
+ - module: community.crypto.openssl_publickey
+'''
+
+EXAMPLES = r'''
+- name: Generate PKCS#12 file
+ community.crypto.openssl_pkcs12:
+ action: export
+ path: /opt/certs/ansible.p12
+ friendly_name: raclette
+ privatekey_path: /opt/certs/keys/key.pem
+ certificate_path: /opt/certs/cert.pem
+ other_certificates: /opt/certs/ca.pem
+ # Note that if /opt/certs/ca.pem contains multiple certificates,
+ # only the first one will be used. See the other_certificates_parse_all
+ # option for changing this behavior.
+ state: present
+
+- name: Generate PKCS#12 file
+ community.crypto.openssl_pkcs12:
+ action: export
+ path: /opt/certs/ansible.p12
+ friendly_name: raclette
+ privatekey_content: '{{ private_key_contents }}'
+ certificate_path: /opt/certs/cert.pem
+ other_certificates_parse_all: true
+ other_certificates:
+ - /opt/certs/ca_bundle.pem
+ # Since we set other_certificates_parse_all to true, all
+ # certificates in the CA bundle are included and not just
+ # the first one.
+ - /opt/certs/intermediate.pem
+ # In case this file has multiple certificates in it,
+ # all will be included as well.
+ state: present
+
+- name: Change PKCS#12 file permission
+ community.crypto.openssl_pkcs12:
+ action: export
+ path: /opt/certs/ansible.p12
+ friendly_name: raclette
+ privatekey_path: /opt/certs/keys/key.pem
+ certificate_path: /opt/certs/cert.pem
+ other_certificates: /opt/certs/ca.pem
+ state: present
+ mode: '0600'
+
+- name: Regen PKCS#12 file
+ community.crypto.openssl_pkcs12:
+ action: export
+ src: /opt/certs/ansible.p12
+ path: /opt/certs/ansible.p12
+ friendly_name: raclette
+ privatekey_path: /opt/certs/keys/key.pem
+ certificate_path: /opt/certs/cert.pem
+ other_certificates: /opt/certs/ca.pem
+ state: present
+ mode: '0600'
+ force: true
+
+- name: Dump/Parse PKCS#12 file
+ community.crypto.openssl_pkcs12:
+ action: parse
+ src: /opt/certs/ansible.p12
+ path: /opt/certs/ansible.pem
+ state: present
+
+- name: Remove PKCS#12 file
+ community.crypto.openssl_pkcs12:
+ path: /opt/certs/ansible.p12
+ state: absent
+'''
+
+RETURN = r'''
+filename:
+ description: Path to the generate PKCS#12 file.
+ returned: changed or success
+ type: str
+ sample: /opt/certs/ansible.p12
+privatekey:
+ description: Path to the TLS/SSL private key the public key was generated from.
+ returned: changed or success
+ type: str
+ sample: /etc/ssl/private/ansible.com.pem
+backup_file:
+ description: Name of backup file created.
+ returned: changed and if I(backup) is C(true)
+ type: str
+ sample: /path/to/ansible.com.pem.2019-03-09@11:22~
+pkcs12:
+ description: The (current or generated) PKCS#12's content Base64 encoded.
+ returned: if I(state) is C(present) and I(return_content) is C(true)
+ type: str
+ version_added: "1.0.0"
+'''
+
+import abc
+import base64
+import os
+import stat
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.common.text.converters import to_bytes, to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.io import (
+ load_file_if_exists,
+ write_file,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+ OpenSSLBadPassphraseError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
+ parse_pkcs12,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ OpenSSLObject,
+ load_privatekey,
+ load_certificate,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
+ split_pem_list,
+)
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '3.0'
+MINIMAL_PYOPENSSL_VERSION = '0.15'
+
+PYOPENSSL_IMP_ERR = None
+try:
+ import OpenSSL
+ from OpenSSL import crypto
+ PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
+except (ImportError, AttributeError):
+ PYOPENSSL_IMP_ERR = traceback.format_exc()
+ PYOPENSSL_FOUND = False
+else:
+ PYOPENSSL_FOUND = True
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ from cryptography.hazmat.primitives import serialization
+ from cryptography.hazmat.primitives.serialization.pkcs12 import serialize_key_and_certificates
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+CRYPTOGRAPHY_COMPATIBILITY2022_ERR = None
+try:
+ from cryptography.hazmat.primitives import hashes
+ from cryptography.hazmat.primitives.serialization.pkcs12 import PBES
+ # Try to build encryption builder for compatibility2022
+ serialization.PrivateFormat.PKCS12.encryption_builder().key_cert_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC).hmac_hash(hashes.SHA1())
+except Exception:
+ CRYPTOGRAPHY_COMPATIBILITY2022_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_HAS_COMPATIBILITY2022 = False
+else:
+ CRYPTOGRAPHY_HAS_COMPATIBILITY2022 = True
+
+
+def load_certificate_set(filename, backend):
+ '''
+ Load list of concatenated PEM files, and return a list of parsed certificates.
+ '''
+ with open(filename, 'rb') as f:
+ data = f.read().decode('utf-8')
+ return [load_certificate(None, content=cert.encode('utf-8'), backend=backend) for cert in split_pem_list(data)]
+
+
+class PkcsError(OpenSSLObjectError):
+ pass
+
+
+class Pkcs(OpenSSLObject):
+ def __init__(self, module, backend, iter_size_default=2048):
+ super(Pkcs, self).__init__(
+ module.params['path'],
+ module.params['state'],
+ module.params['force'],
+ module.check_mode
+ )
+ self.backend = backend
+ self.action = module.params['action']
+ self.other_certificates = module.params['other_certificates']
+ self.other_certificates_parse_all = module.params['other_certificates_parse_all']
+ self.certificate_path = module.params['certificate_path']
+ self.friendly_name = module.params['friendly_name']
+ self.iter_size = module.params['iter_size'] or iter_size_default
+ self.maciter_size = module.params['maciter_size'] or 1
+ self.encryption_level = module.params['encryption_level']
+ self.passphrase = module.params['passphrase']
+ self.pkcs12 = None
+ self.privatekey_passphrase = module.params['privatekey_passphrase']
+ self.privatekey_path = module.params['privatekey_path']
+ self.privatekey_content = module.params['privatekey_content']
+ self.pkcs12_bytes = None
+ self.return_content = module.params['return_content']
+ self.src = module.params['src']
+
+ if module.params['mode'] is None:
+ module.params['mode'] = '0400'
+
+ self.backup = module.params['backup']
+ self.backup_file = None
+
+ if self.privatekey_path is not None:
+ try:
+ with open(self.privatekey_path, 'rb') as fh:
+ self.privatekey_content = fh.read()
+ except (IOError, OSError) as exc:
+ raise PkcsError(exc)
+ elif self.privatekey_content is not None:
+ self.privatekey_content = to_bytes(self.privatekey_content)
+
+ if self.other_certificates:
+ if self.other_certificates_parse_all:
+ filenames = list(self.other_certificates)
+ self.other_certificates = []
+ for other_cert_bundle in filenames:
+ self.other_certificates.extend(load_certificate_set(other_cert_bundle, self.backend))
+ else:
+ self.other_certificates = [
+ load_certificate(other_cert, backend=self.backend) for other_cert in self.other_certificates
+ ]
+
+ @abc.abstractmethod
+ def generate_bytes(self, module):
+ """Generate PKCS#12 file archive."""
+ pass
+
+ @abc.abstractmethod
+ def parse_bytes(self, pkcs12_content):
+ pass
+
+ @abc.abstractmethod
+ def _dump_privatekey(self, pkcs12):
+ pass
+
+ @abc.abstractmethod
+ def _dump_certificate(self, pkcs12):
+ pass
+
+ @abc.abstractmethod
+ def _dump_other_certificates(self, pkcs12):
+ pass
+
+ @abc.abstractmethod
+ def _get_friendly_name(self, pkcs12):
+ pass
+
+ def check(self, module, perms_required=True):
+ """Ensure the resource is in its desired state."""
+
+ state_and_perms = super(Pkcs, self).check(module, perms_required)
+
+ def _check_pkey_passphrase():
+ if self.privatekey_passphrase:
+ try:
+ load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend)
+ except OpenSSLObjectError:
+ return False
+ return True
+
+ if not state_and_perms:
+ return state_and_perms
+
+ if os.path.exists(self.path) and module.params['action'] == 'export':
+ dummy = self.generate_bytes(module)
+ self.src = self.path
+ try:
+ pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse()
+ except OpenSSLObjectError:
+ return False
+ if (pkcs12_privatekey is not None) and (self.privatekey_content is not None):
+ expected_pkey = self._dump_privatekey(self.pkcs12)
+ if pkcs12_privatekey != expected_pkey:
+ return False
+ elif bool(pkcs12_privatekey) != bool(self.privatekey_content):
+ return False
+
+ if (pkcs12_certificate is not None) and (self.certificate_path is not None):
+ expected_cert = self._dump_certificate(self.pkcs12)
+ if pkcs12_certificate != expected_cert:
+ return False
+ elif bool(pkcs12_certificate) != bool(self.certificate_path):
+ return False
+
+ if (pkcs12_other_certificates is not None) and (self.other_certificates is not None):
+ expected_other_certs = self._dump_other_certificates(self.pkcs12)
+ if set(pkcs12_other_certificates) != set(expected_other_certs):
+ return False
+ elif bool(pkcs12_other_certificates) != bool(self.other_certificates):
+ return False
+
+ if pkcs12_privatekey:
+ # This check is required because pyOpenSSL will not return a friendly name
+ # if the private key is not set in the file
+ friendly_name = self._get_friendly_name(self.pkcs12)
+ if ((friendly_name is not None) and (pkcs12_friendly_name is not None)):
+ if friendly_name != pkcs12_friendly_name:
+ return False
+ elif bool(friendly_name) != bool(pkcs12_friendly_name):
+ return False
+ elif module.params['action'] == 'parse' and os.path.exists(self.src) and os.path.exists(self.path):
+ try:
+ pkey, cert, other_certs, friendly_name = self.parse()
+ except OpenSSLObjectError:
+ return False
+ expected_content = to_bytes(
+ ''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None])
+ )
+ dumped_content = load_file_if_exists(self.path, ignore_errors=True)
+ if expected_content != dumped_content:
+ return False
+ else:
+ return False
+
+ return _check_pkey_passphrase()
+
+ def dump(self):
+ """Serialize the object into a dictionary."""
+
+ result = {
+ 'filename': self.path,
+ }
+ if self.privatekey_path:
+ result['privatekey_path'] = self.privatekey_path
+ if self.backup_file:
+ result['backup_file'] = self.backup_file
+ if self.return_content:
+ if self.pkcs12_bytes is None:
+ self.pkcs12_bytes = load_file_if_exists(self.path, ignore_errors=True)
+ result['pkcs12'] = base64.b64encode(self.pkcs12_bytes) if self.pkcs12_bytes else None
+
+ return result
+
+ def remove(self, module):
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ super(Pkcs, self).remove(module)
+
+ def parse(self):
+ """Read PKCS#12 file."""
+
+ try:
+ with open(self.src, 'rb') as pkcs12_fh:
+ pkcs12_content = pkcs12_fh.read()
+ return self.parse_bytes(pkcs12_content)
+ except IOError as exc:
+ raise PkcsError(exc)
+
+ def generate(self):
+ pass
+
+ def write(self, module, content, mode=None):
+ """Write the PKCS#12 file."""
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ write_file(module, content, mode)
+ if self.return_content:
+ self.pkcs12_bytes = content
+
+
+class PkcsPyOpenSSL(Pkcs):
+ def __init__(self, module):
+ super(PkcsPyOpenSSL, self).__init__(module, 'pyopenssl')
+ if self.encryption_level != 'auto':
+ module.fail_json(msg='The PyOpenSSL backend only supports encryption_level = auto')
+
+ def generate_bytes(self, module):
+ """Generate PKCS#12 file archive."""
+ self.pkcs12 = crypto.PKCS12()
+
+ if self.other_certificates:
+ self.pkcs12.set_ca_certificates(self.other_certificates)
+
+ if self.certificate_path:
+ self.pkcs12.set_certificate(load_certificate(self.certificate_path, backend=self.backend))
+
+ if self.friendly_name:
+ self.pkcs12.set_friendlyname(to_bytes(self.friendly_name))
+
+ if self.privatekey_content:
+ try:
+ self.pkcs12.set_privatekey(
+ load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend))
+ except OpenSSLBadPassphraseError as exc:
+ raise PkcsError(exc)
+
+ return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size)
+
+ def parse_bytes(self, pkcs12_content):
+ try:
+ p12 = crypto.load_pkcs12(pkcs12_content, self.passphrase)
+ pkey = p12.get_privatekey()
+ if pkey is not None:
+ pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
+ crt = p12.get_certificate()
+ if crt is not None:
+ crt = crypto.dump_certificate(crypto.FILETYPE_PEM, crt)
+ other_certs = []
+ if p12.get_ca_certificates() is not None:
+ other_certs = [crypto.dump_certificate(crypto.FILETYPE_PEM,
+ other_cert) for other_cert in p12.get_ca_certificates()]
+
+ friendly_name = p12.get_friendlyname()
+
+ return (pkey, crt, other_certs, friendly_name)
+ except crypto.Error as exc:
+ raise PkcsError(exc)
+
+ def _dump_privatekey(self, pkcs12):
+ pk = pkcs12.get_privatekey()
+ return crypto.dump_privatekey(crypto.FILETYPE_PEM, pk) if pk else None
+
+ def _dump_certificate(self, pkcs12):
+ cert = pkcs12.get_certificate()
+ return crypto.dump_certificate(crypto.FILETYPE_PEM, cert) if cert else None
+
+ def _dump_other_certificates(self, pkcs12):
+ if pkcs12.get_ca_certificates() is None:
+ return []
+ return [
+ crypto.dump_certificate(crypto.FILETYPE_PEM, other_cert)
+ for other_cert in pkcs12.get_ca_certificates()
+ ]
+
+ def _get_friendly_name(self, pkcs12):
+ return pkcs12.get_friendlyname()
+
+
+class PkcsCryptography(Pkcs):
+ def __init__(self, module):
+ super(PkcsCryptography, self).__init__(module, 'cryptography', iter_size_default=50000)
+ if self.encryption_level == 'compatibility2022' and not CRYPTOGRAPHY_HAS_COMPATIBILITY2022:
+ module.fail_json(
+ msg='The installed cryptography version does not support encryption_level = compatibility2022.'
+ ' You need cryptography >= 38.0.0 and support for SHA1',
+ exception=CRYPTOGRAPHY_COMPATIBILITY2022_ERR)
+
+ def generate_bytes(self, module):
+ """Generate PKCS#12 file archive."""
+ pkey = None
+ if self.privatekey_content:
+ try:
+ pkey = load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend)
+ except OpenSSLBadPassphraseError as exc:
+ raise PkcsError(exc)
+
+ cert = None
+ if self.certificate_path:
+ cert = load_certificate(self.certificate_path, backend=self.backend)
+
+ friendly_name = to_bytes(self.friendly_name) if self.friendly_name is not None else None
+
+ # Store fake object which can be used to retrieve the components back
+ self.pkcs12 = (pkey, cert, self.other_certificates, friendly_name)
+
+ if not self.passphrase:
+ encryption = serialization.NoEncryption()
+ elif self.encryption_level == 'compatibility2022':
+ encryption = (
+ serialization.PrivateFormat.PKCS12.encryption_builder().
+ kdf_rounds(self.iter_size).
+ key_cert_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC).
+ hmac_hash(hashes.SHA1()).
+ build(to_bytes(self.passphrase))
+ )
+ else:
+ encryption = serialization.BestAvailableEncryption(to_bytes(self.passphrase))
+
+ return serialize_key_and_certificates(
+ friendly_name,
+ pkey,
+ cert,
+ self.other_certificates,
+ encryption,
+ )
+
+ def parse_bytes(self, pkcs12_content):
+ try:
+ private_key, certificate, additional_certificates, friendly_name = parse_pkcs12(
+ pkcs12_content, self.passphrase)
+
+ pkey = None
+ if private_key is not None:
+ pkey = private_key.private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption(),
+ )
+
+ crt = None
+ if certificate is not None:
+ crt = certificate.public_bytes(serialization.Encoding.PEM)
+
+ other_certs = []
+ if additional_certificates is not None:
+ other_certs = [
+ other_cert.public_bytes(serialization.Encoding.PEM)
+ for other_cert in additional_certificates
+ ]
+
+ return (pkey, crt, other_certs, friendly_name)
+ except ValueError as exc:
+ raise PkcsError(exc)
+
+ # The following methods will get self.pkcs12 passed, which is computed as:
+ #
+ # self.pkcs12 = (pkey, cert, self.other_certificates, self.friendly_name)
+
+ def _dump_privatekey(self, pkcs12):
+ return pkcs12[0].private_bytes(
+ encoding=serialization.Encoding.PEM,
+ format=serialization.PrivateFormat.TraditionalOpenSSL,
+ encryption_algorithm=serialization.NoEncryption(),
+ ) if pkcs12[0] else None
+
+ def _dump_certificate(self, pkcs12):
+ return pkcs12[1].public_bytes(serialization.Encoding.PEM) if pkcs12[1] else None
+
+ def _dump_other_certificates(self, pkcs12):
+ return [other_cert.public_bytes(serialization.Encoding.PEM) for other_cert in pkcs12[2]]
+
+ def _get_friendly_name(self, pkcs12):
+ return pkcs12[3]
+
+
+def select_backend(module, backend):
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
+ can_use_pyopenssl = PYOPENSSL_FOUND and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
+
+ # If no restrictions are provided, first try cryptography, then pyOpenSSL
+ if (
+ (module.params['iter_size'] is not None and module.params['encryption_level'] != 'compatibility2022')
+ or module.params['maciter_size'] is not None
+ ):
+ # If iter_size (for encryption_level != compatibility2022) or maciter_size is specified, use pyOpenSSL backend
+ backend = 'pyopenssl'
+ elif can_use_cryptography:
+ backend = 'cryptography'
+ elif can_use_pyopenssl:
+ backend = 'pyopenssl'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect any of the required Python libraries "
+ "cryptography (>= {0}) or PyOpenSSL (>= {1})").format(
+ MINIMAL_CRYPTOGRAPHY_VERSION,
+ MINIMAL_PYOPENSSL_VERSION))
+
+ if backend == 'pyopenssl':
+ if not PYOPENSSL_FOUND:
+ module.fail_json(msg=missing_required_lib('pyOpenSSL >= {0}'.format(MINIMAL_PYOPENSSL_VERSION)),
+ exception=PYOPENSSL_IMP_ERR)
+ # module.deprecate('The module is using the PyOpenSSL backend. This backend has been deprecated',
+ # version='x.0.0', collection_name='community.crypto')
+ return backend, PkcsPyOpenSSL(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)
+ return backend, PkcsCryptography(module)
+ else:
+ raise ValueError('Unsupported value for backend: {0}'.format(backend))
+
+
+def main():
+ argument_spec = dict(
+ action=dict(type='str', default='export', choices=['export', 'parse']),
+ other_certificates=dict(type='list', elements='path', aliases=['ca_certificates']),
+ other_certificates_parse_all=dict(type='bool', default=False),
+ certificate_path=dict(type='path'),
+ force=dict(type='bool', default=False),
+ friendly_name=dict(type='str', aliases=['name']),
+ encryption_level=dict(type='str', choices=['auto', 'compatibility2022'], default='auto'),
+ iter_size=dict(type='int'),
+ maciter_size=dict(type='int'),
+ passphrase=dict(type='str', no_log=True),
+ path=dict(type='path', required=True),
+ privatekey_passphrase=dict(type='str', no_log=True),
+ privatekey_path=dict(type='path'),
+ privatekey_content=dict(type='str', no_log=True),
+ state=dict(type='str', default='present', choices=['absent', 'present']),
+ src=dict(type='path'),
+ backup=dict(type='bool', default=False),
+ return_content=dict(type='bool', default=False),
+ select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'pyopenssl']),
+ )
+
+ required_if = [
+ ['action', 'parse', ['src']],
+ ]
+
+ mutually_exclusive = [
+ ['privatekey_path', 'privatekey_content'],
+ ]
+
+ module = AnsibleModule(
+ add_file_common_args=True,
+ argument_spec=argument_spec,
+ required_if=required_if,
+ mutually_exclusive=mutually_exclusive,
+ supports_check_mode=True,
+ )
+
+ backend, pkcs12 = select_backend(module, module.params['select_crypto_backend'])
+
+ base_dir = os.path.dirname(module.params['path']) or '.'
+ if not os.path.isdir(base_dir):
+ module.fail_json(
+ name=base_dir,
+ msg="The directory '%s' does not exist or the path is not a directory" % base_dir
+ )
+
+ try:
+ changed = False
+
+ if module.params['state'] == 'present':
+ if module.check_mode:
+ result = pkcs12.dump()
+ result['changed'] = module.params['force'] or not pkcs12.check(module)
+ module.exit_json(**result)
+
+ if not pkcs12.check(module, perms_required=False) or module.params['force']:
+ if module.params['action'] == 'export':
+ if not module.params['friendly_name']:
+ module.fail_json(msg='Friendly_name is required')
+ pkcs12_content = pkcs12.generate_bytes(module)
+ pkcs12.write(module, pkcs12_content, 0o600)
+ changed = True
+ else:
+ pkey, cert, other_certs, friendly_name = pkcs12.parse()
+ dump_content = ''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None])
+ pkcs12.write(module, to_bytes(dump_content))
+ changed = True
+
+ file_args = module.load_file_common_arguments(module.params)
+ if module.check_file_absent_if_check_mode(file_args['path']):
+ changed = True
+ elif module.set_fs_attributes_if_different(file_args, changed):
+ changed = True
+ else:
+ if module.check_mode:
+ result = pkcs12.dump()
+ result['changed'] = os.path.exists(module.params['path'])
+ module.exit_json(**result)
+
+ if os.path.exists(module.params['path']):
+ pkcs12.remove(module)
+ changed = True
+
+ result = pkcs12.dump()
+ result['changed'] = changed
+ if os.path.exists(module.params['path']):
+ file_mode = "%04o" % stat.S_IMODE(os.stat(module.params['path']).st_mode)
+ result['mode'] = file_mode
+
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_privatekey.py b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey.py
new file mode 100644
index 00000000..7b50caff
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey.py
@@ -0,0 +1,290 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_privatekey
+short_description: Generate OpenSSL private keys
+description:
+ - This module allows one to (re)generate OpenSSL private keys.
+ - The default mode for the private key file will be C(0600) if I(mode) is not explicitly set.
+author:
+ - Yanis Guenane (@Spredzy)
+ - Felix Fontein (@felixfontein)
+extends_documentation_fragment:
+ - ansible.builtin.files
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+ - community.crypto.module_privatekey
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+ safe_file_operations:
+ support: full
+options:
+ state:
+ description:
+ - Whether the private key should exist or not, taking action if the state is different from what is stated.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ force:
+ description:
+ - Should the key be regenerated even if it already exists.
+ type: bool
+ default: false
+ path:
+ description:
+ - Name of the file in which the generated TLS/SSL private key will be written. It will have C(0600) mode
+ if I(mode) is not explicitly set.
+ type: path
+ required: true
+ format:
+ version_added: '1.0.0'
+ format_mismatch:
+ version_added: '1.0.0'
+ backup:
+ description:
+ - Create a backup file including a timestamp so you can get
+ the original private key back if you overwrote it with a new one by accident.
+ type: bool
+ default: false
+ return_content:
+ description:
+ - If set to C(true), will return the (current or generated) private key's content as I(privatekey).
+ - Note that especially if the private key is not encrypted, you have to make sure that the returned
+ value is treated appropriately and not accidentally written to logs etc.! Use with care!
+ - Use Ansible's I(no_log) task option to avoid the output being shown. See also
+ U(https://docs.ansible.com/ansible/latest/reference_appendices/faq.html#how-do-i-keep-secret-data-in-my-playbook).
+ type: bool
+ default: false
+ version_added: '1.0.0'
+ regenerate:
+ version_added: '1.0.0'
+seealso:
+ - module: community.crypto.openssl_privatekey_pipe
+ - module: community.crypto.openssl_privatekey_info
+'''
+
+EXAMPLES = r'''
+- name: Generate an OpenSSL private key with the default values (4096 bits, RSA)
+ community.crypto.openssl_privatekey:
+ path: /etc/ssl/private/ansible.com.pem
+
+- name: Generate an OpenSSL private key with the default values (4096 bits, RSA) and a passphrase
+ community.crypto.openssl_privatekey:
+ path: /etc/ssl/private/ansible.com.pem
+ passphrase: ansible
+ cipher: auto
+
+- name: Generate an OpenSSL private key with a different size (2048 bits)
+ community.crypto.openssl_privatekey:
+ path: /etc/ssl/private/ansible.com.pem
+ size: 2048
+
+- name: Force regenerate an OpenSSL private key if it already exists
+ community.crypto.openssl_privatekey:
+ path: /etc/ssl/private/ansible.com.pem
+ force: true
+
+- name: Generate an OpenSSL private key with a different algorithm (DSA)
+ community.crypto.openssl_privatekey:
+ path: /etc/ssl/private/ansible.com.pem
+ type: DSA
+'''
+
+RETURN = r'''
+size:
+ description: Size (in bits) of the TLS/SSL private key.
+ returned: changed or success
+ type: int
+ sample: 4096
+type:
+ description: Algorithm used to generate the TLS/SSL private key.
+ returned: changed or success
+ type: str
+ sample: RSA
+curve:
+ description: Elliptic curve used to generate the TLS/SSL private key.
+ returned: changed or success, and I(type) is C(ECC)
+ type: str
+ sample: secp256r1
+filename:
+ description: Path to the generated TLS/SSL private key file.
+ returned: changed or success
+ type: str
+ sample: /etc/ssl/private/ansible.com.pem
+fingerprint:
+ description:
+ - The fingerprint of the public key. Fingerprint will be generated for each C(hashlib.algorithms) available.
+ returned: changed or success
+ type: dict
+ sample:
+ md5: "84:75:71:72:8d:04:b5:6c:4d:37:6d:66:83:f5:4c:29"
+ sha1: "51:cc:7c:68:5d:eb:41:43:88:7e:1a:ae:c7:f8:24:72:ee:71:f6:10"
+ sha224: "b1:19:a6:6c:14:ac:33:1d:ed:18:50:d3:06:5c:b2:32:91:f1:f1:52:8c:cb:d5:75:e9:f5:9b:46"
+ sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7"
+ sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d"
+ sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b"
+backup_file:
+ description: Name of backup file created.
+ returned: changed and if I(backup) is C(true)
+ type: str
+ sample: /path/to/privatekey.pem.2019-03-09@11:22~
+privatekey:
+ description:
+ - The (current or generated) private key's content.
+ - Will be Base64-encoded if the key is in raw format.
+ returned: if I(state) is C(present) and I(return_content) is C(true)
+ type: str
+ version_added: '1.0.0'
+'''
+
+import os
+
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.io import (
+ load_file_if_exists,
+ write_file,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ OpenSSLObject,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey import (
+ select_backend,
+ get_privatekey_argument_spec,
+)
+
+
+class PrivateKeyModule(OpenSSLObject):
+
+ def __init__(self, module, module_backend):
+ super(PrivateKeyModule, self).__init__(
+ module.params['path'],
+ module.params['state'],
+ module.params['force'],
+ module.check_mode,
+ )
+ self.module_backend = module_backend
+ self.return_content = module.params['return_content']
+ if self.force:
+ module_backend.regenerate = 'always'
+
+ self.backup = module.params['backup']
+ self.backup_file = None
+
+ if module.params['mode'] is None:
+ module.params['mode'] = '0600'
+
+ module_backend.set_existing(load_file_if_exists(self.path, module))
+
+ def generate(self, module):
+ """Generate a keypair."""
+
+ if self.module_backend.needs_regeneration():
+ # Regenerate
+ if not self.check_mode:
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ self.module_backend.generate_private_key()
+ privatekey_data = self.module_backend.get_private_key_data()
+ if self.return_content:
+ self.privatekey_bytes = privatekey_data
+ write_file(module, privatekey_data, 0o600)
+ self.changed = True
+ elif self.module_backend.needs_conversion():
+ # Convert
+ if not self.check_mode:
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ self.module_backend.convert_private_key()
+ privatekey_data = self.module_backend.get_private_key_data()
+ if self.return_content:
+ self.privatekey_bytes = privatekey_data
+ write_file(module, privatekey_data, 0o600)
+ self.changed = True
+
+ file_args = module.load_file_common_arguments(module.params)
+ if module.check_file_absent_if_check_mode(file_args['path']):
+ self.changed = True
+ else:
+ self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
+
+ def remove(self, module):
+ self.module_backend.set_existing(None)
+ if self.backup and not self.check_mode:
+ self.backup_file = module.backup_local(self.path)
+ super(PrivateKeyModule, self).remove(module)
+
+ def dump(self):
+ """Serialize the object into a dictionary."""
+
+ result = self.module_backend.dump(include_key=self.return_content)
+ result['filename'] = self.path
+ result['changed'] = self.changed
+ if self.backup_file:
+ result['backup_file'] = self.backup_file
+
+ return result
+
+
+def main():
+
+ argument_spec = get_privatekey_argument_spec()
+ argument_spec.argument_spec.update(dict(
+ state=dict(type='str', default='present', choices=['present', 'absent']),
+ force=dict(type='bool', default=False),
+ path=dict(type='path', required=True),
+ backup=dict(type='bool', default=False),
+ return_content=dict(type='bool', default=False),
+ ))
+ module = argument_spec.create_ansible_module(
+ supports_check_mode=True,
+ add_file_common_args=True,
+ )
+
+ base_dir = os.path.dirname(module.params['path']) or '.'
+ if not os.path.isdir(base_dir):
+ module.fail_json(
+ name=base_dir,
+ msg='The directory %s does not exist or the file is not a directory' % base_dir
+ )
+
+ backend, module_backend = select_backend(
+ module=module,
+ backend=module.params['select_crypto_backend'],
+ )
+
+ try:
+ private_key = PrivateKeyModule(module, module_backend)
+
+ if private_key.state == 'present':
+ private_key.generate(module)
+ else:
+ private_key.remove(module)
+
+ result = private_key.dump()
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_convert.py b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_convert.py
new file mode 100644
index 00000000..5aec5cbe
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_convert.py
@@ -0,0 +1,171 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2022, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_privatekey_convert
+short_description: Convert OpenSSL private keys
+version_added: 2.1.0
+description:
+ - This module allows one to convert OpenSSL private keys.
+ - The default mode for the private key file will be C(0600) if I(mode) is not explicitly set.
+author:
+ - Felix Fontein (@felixfontein)
+extends_documentation_fragment:
+ - ansible.builtin.files
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+ - community.crypto.module_privatekey_convert
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: none
+ safe_file_operations:
+ support: full
+options:
+ dest_path:
+ description:
+ - Name of the file in which the generated TLS/SSL private key will be written. It will have C(0600) mode
+ if I(mode) is not explicitly set.
+ type: path
+ required: true
+ backup:
+ description:
+ - Create a backup file including a timestamp so you can get
+ the original private key back if you overwrote it with a new one by accident.
+ type: bool
+ default: false
+seealso: []
+'''
+
+EXAMPLES = r'''
+- name: Convert private key to PKCS8 format with passphrase
+ community.crypto.openssl_privatekey_convert:
+ src_path: /etc/ssl/private/ansible.com.pem
+ dest_path: /etc/ssl/private/ansible.com.key
+ dest_passphrase: '{{ private_key_passphrase }}'
+ format: pkcs8
+'''
+
+RETURN = r'''
+backup_file:
+ description: Name of backup file created.
+ returned: changed and if I(backup) is C(true)
+ type: str
+ sample: /path/to/privatekey.pem.2019-03-09@11:22~
+'''
+
+import os
+
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.io import (
+ load_file_if_exists,
+ write_file,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ OpenSSLObject,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_convert import (
+ select_backend,
+ get_privatekey_argument_spec,
+)
+
+
+class PrivateKeyConvertModule(OpenSSLObject):
+ def __init__(self, module, module_backend):
+ super(PrivateKeyConvertModule, self).__init__(
+ module.params['dest_path'],
+ 'present',
+ False,
+ module.check_mode,
+ )
+ self.module_backend = module_backend
+
+ self.backup = module.params['backup']
+ self.backup_file = None
+
+ module.params['path'] = module.params['dest_path']
+ if module.params['mode'] is None:
+ module.params['mode'] = '0600'
+
+ module_backend.set_existing_destination(load_file_if_exists(self.path, module))
+
+ def generate(self, module):
+ """Do conversion."""
+
+ if self.module_backend.needs_conversion():
+ # Convert
+ privatekey_data = self.module_backend.get_private_key_data()
+ if not self.check_mode:
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ write_file(module, privatekey_data, 0o600)
+ self.changed = True
+
+ file_args = module.load_file_common_arguments(module.params)
+ if module.check_file_absent_if_check_mode(file_args['path']):
+ self.changed = True
+ else:
+ self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
+
+ def dump(self):
+ """Serialize the object into a dictionary."""
+
+ result = self.module_backend.dump()
+ result['changed'] = self.changed
+ if self.backup_file:
+ result['backup_file'] = self.backup_file
+
+ return result
+
+
+def main():
+
+ argument_spec = get_privatekey_argument_spec()
+ argument_spec.argument_spec.update(dict(
+ dest_path=dict(type='path', required=True),
+ backup=dict(type='bool', default=False),
+ ))
+ module = argument_spec.create_ansible_module(
+ supports_check_mode=True,
+ add_file_common_args=True,
+ )
+
+ base_dir = os.path.dirname(module.params['dest_path']) or '.'
+ if not os.path.isdir(base_dir):
+ module.fail_json(
+ name=base_dir,
+ msg='The directory %s does not exist or the file is not a directory' % base_dir
+ )
+
+ module_backend = select_backend(module=module)
+
+ try:
+ private_key = PrivateKeyConvertModule(module, module_backend)
+
+ private_key.generate(module)
+
+ result = private_key.dump()
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_info.py b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_info.py
new file mode 100644
index 00000000..7eaec234
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_info.py
@@ -0,0 +1,278 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_privatekey_info
+short_description: Provide information for OpenSSL private keys
+description:
+ - This module allows one to query information on OpenSSL private keys.
+ - In case the key consistency checks fail, the module will fail as this indicates a faked
+ private key. In this case, all return variables are still returned. Note that key consistency
+ checks are not available all key types; if none is available, C(none) is returned for
+ C(key_is_consistent).
+ - It uses the cryptography python library to interact with OpenSSL.
+requirements:
+ - cryptography >= 1.2.3
+author:
+ - Felix Fontein (@felixfontein)
+ - Yanis Guenane (@Spredzy)
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - community.crypto.attributes.info_module
+options:
+ path:
+ description:
+ - Remote absolute path where the private key file is loaded from.
+ type: path
+ content:
+ description:
+ - Content of the private key file.
+ - Either I(path) or I(content) must be specified, but not both.
+ type: str
+ version_added: '1.0.0'
+ passphrase:
+ description:
+ - The passphrase for the private key.
+ type: str
+ return_private_key_data:
+ description:
+ - Whether to return private key data.
+ - Only set this to C(true) when you want private information about this key to
+ leave the remote machine.
+ - "B(WARNING:) you have to make sure that private key data is not accidentally logged!"
+ type: bool
+ default: false
+ check_consistency:
+ description:
+ - Whether to check consistency of the private key.
+ - In community.crypto < 2.0.0, consistency was always checked.
+ - Since community.crypto 2.0.0, the consistency check has been disabled by default to
+ avoid private key material to be transported around and computed with, and only do
+ so when requested explicitly. This can potentially prevent
+ L(side-channel attacks,https://en.wikipedia.org/wiki/Side-channel_attack).
+ type: bool
+ default: false
+ version_added: 2.0.0
+
+ select_crypto_backend:
+ description:
+ - Determines which crypto backend to use.
+ - The default choice is C(auto), which tries to use C(cryptography) if available.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography ]
+
+seealso:
+ - module: community.crypto.openssl_privatekey
+ - module: community.crypto.openssl_privatekey_pipe
+ - ref: community.crypto.openssl_privatekey_info filter <ansible_collections.community.crypto.openssl_privatekey_info_filter>
+ # - plugin: community.crypto.openssl_privatekey_info
+ # plugin_type: filter
+ description: A filter variant of this module.
+'''
+
+EXAMPLES = r'''
+- name: Generate an OpenSSL private key with the default values (4096 bits, RSA)
+ community.crypto.openssl_privatekey:
+ path: /etc/ssl/private/ansible.com.pem
+
+- name: Get information on generated key
+ community.crypto.openssl_privatekey_info:
+ path: /etc/ssl/private/ansible.com.pem
+ register: result
+
+- name: Dump information
+ ansible.builtin.debug:
+ var: result
+'''
+
+RETURN = r'''
+can_load_key:
+ description: Whether the module was able to load the private key from disk.
+ returned: always
+ type: bool
+can_parse_key:
+ description: Whether the module was able to parse the private key.
+ returned: always
+ type: bool
+key_is_consistent:
+ description:
+ - Whether the key is consistent. Can also return C(none) next to C(true) and
+ C(false), to indicate that consistency could not be checked.
+ - In case the check returns C(false), the module will fail.
+ returned: when I(check_consistency=true)
+ type: bool
+public_key:
+ description: Private key's public key in PEM format.
+ returned: success
+ type: str
+ sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
+public_key_fingerprints:
+ description:
+ - Fingerprints of private key's public key.
+ - For every hash algorithm available, the fingerprint is computed.
+ returned: success
+ type: dict
+ sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
+ 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
+type:
+ description:
+ - The key's type.
+ - One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
+ - Will start with C(unknown) if the key type cannot be determined.
+ returned: success
+ type: str
+ sample: RSA
+public_data:
+ description:
+ - Public key data. Depends on key type.
+ returned: success
+ type: dict
+ contains:
+ size:
+ description:
+ - Bit size of modulus (RSA) or prime number (DSA).
+ type: int
+ returned: When C(type=RSA) or C(type=DSA)
+ modulus:
+ description:
+ - The RSA key's modulus.
+ type: int
+ returned: When C(type=RSA)
+ exponent:
+ description:
+ - The RSA key's public exponent.
+ type: int
+ returned: When C(type=RSA)
+ p:
+ description:
+ - The C(p) value for DSA.
+ - This is the prime modulus upon which arithmetic takes place.
+ type: int
+ returned: When C(type=DSA)
+ q:
+ description:
+ - The C(q) value for DSA.
+ - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
+ multiplicative group of the prime field used.
+ type: int
+ returned: When C(type=DSA)
+ g:
+ description:
+ - The C(g) value for DSA.
+ - This is the element spanning the subgroup of the multiplicative group of the prime field used.
+ type: int
+ returned: When C(type=DSA)
+ curve:
+ description:
+ - The curve's name for ECC.
+ type: str
+ returned: When C(type=ECC)
+ exponent_size:
+ description:
+ - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
+ type: int
+ returned: When C(type=ECC)
+ x:
+ description:
+ - The C(x) coordinate for the public point on the elliptic curve.
+ type: int
+ returned: When C(type=ECC)
+ y:
+ description:
+ - For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
+ - For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
+ type: int
+ returned: When C(type=DSA) or C(type=ECC)
+private_data:
+ description:
+ - Private key data. Depends on key type.
+ returned: success and when I(return_private_key_data) is set to C(true)
+ type: dict
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.privatekey_info import (
+ PrivateKeyConsistencyError,
+ PrivateKeyParseError,
+ select_backend,
+)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ path=dict(type='path'),
+ content=dict(type='str', no_log=True),
+ passphrase=dict(type='str', no_log=True),
+ return_private_key_data=dict(type='bool', default=False),
+ check_consistency=dict(type='bool', default=False),
+ select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
+ ),
+ required_one_of=(
+ ['path', 'content'],
+ ),
+ mutually_exclusive=(
+ ['path', 'content'],
+ ),
+ supports_check_mode=True,
+ )
+
+ result = dict(
+ can_load_key=False,
+ can_parse_key=False,
+ key_is_consistent=None,
+ )
+
+ if module.params['content'] is not None:
+ data = module.params['content'].encode('utf-8')
+ else:
+ try:
+ with open(module.params['path'], 'rb') as f:
+ data = f.read()
+ except (IOError, OSError) as e:
+ module.fail_json(msg='Error while reading private key file from disk: {0}'.format(e), **result)
+
+ result['can_load_key'] = True
+
+ backend, module_backend = select_backend(
+ module,
+ module.params['select_crypto_backend'],
+ data,
+ passphrase=module.params['passphrase'],
+ return_private_key_data=module.params['return_private_key_data'],
+ check_consistency=module.params['check_consistency'])
+
+ try:
+ result.update(module_backend.get_info())
+ module.exit_json(**result)
+ except PrivateKeyParseError as exc:
+ result.update(exc.result)
+ module.fail_json(msg=exc.error_message, **result)
+ except PrivateKeyConsistencyError as exc:
+ result.update(exc.result)
+ module.fail_json(msg=exc.error_message, **result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_pipe.py b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_pipe.py
new file mode 100644
index 00000000..94fc3826
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_privatekey_pipe.py
@@ -0,0 +1,131 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_privatekey_pipe
+short_description: Generate OpenSSL private keys without disk access
+version_added: 1.3.0
+description:
+ - This module allows one to (re)generate OpenSSL private keys without disk access.
+ - This allows to read and write keys to vaults without having to write intermediate versions to disk.
+ - Make sure to not write the result of this module into logs or to the console, as it contains private key data! Use the I(no_log) task option to be sure.
+ - Note that this module is implemented as an L(action plugin,https://docs.ansible.com/ansible/latest/plugins/action.html)
+ and will always be executed on the controller.
+author:
+ - Yanis Guenane (@Spredzy)
+ - Felix Fontein (@felixfontein)
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - community.crypto.attributes.flow
+ - community.crypto.module_privatekey
+attributes:
+ action:
+ support: full
+ async:
+ support: none
+ details:
+ - This action runs completely on the controller.
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+options:
+ content:
+ description:
+ - The current private key data.
+ - Needed for idempotency. If not provided, the module will always return a change, and all idempotence-related
+ options are ignored.
+ type: str
+ content_base64:
+ description:
+ - Set to C(true) if the content is base64 encoded.
+ type: bool
+ default: false
+ return_current_key:
+ description:
+ - Set to C(true) to return the current private key when the module did not generate a new one.
+ - Note that in case of check mode, when this option is not set to C(true), the module always returns the
+ current key (if it was provided) and Ansible will replace it by C(VALUE_SPECIFIED_IN_NO_LOG_PARAMETER).
+ type: bool
+ default: false
+seealso:
+ - module: community.crypto.openssl_privatekey
+ - module: community.crypto.openssl_privatekey_info
+'''
+
+EXAMPLES = r'''
+- name: Generate an OpenSSL private key with the default values (4096 bits, RSA)
+ community.crypto.openssl_privatekey_pipe:
+ path: /etc/ssl/private/ansible.com.pem
+ register: output
+ no_log: true # make sure that private key data is not accidentally revealed in logs!
+- name: Show generated key
+ debug:
+ msg: "{{ output.privatekey }}"
+ # DO NOT OUTPUT KEY MATERIAL TO CONSOLE OR LOGS IN PRODUCTION!
+
+- block:
+ - name: Update sops-encrypted key with the community.sops collection
+ community.crypto.openssl_privatekey_pipe:
+ content: "{{ lookup('community.sops.sops', 'private_key.pem.sops') }}"
+ size: 2048
+ register: output
+ no_log: true # make sure that private key data is not accidentally revealed in logs!
+
+ - name: Update encrypted key when openssl_privatekey_pipe reported a change
+ community.sops.sops_encrypt:
+ path: private_key.pem.sops
+ content_text: "{{ output.privatekey }}"
+ when: output is changed
+ always:
+ - name: Make sure that output (which contains the private key) is overwritten
+ set_fact:
+ output: ''
+'''
+
+RETURN = r'''
+size:
+ description: Size (in bits) of the TLS/SSL private key.
+ returned: changed or success
+ type: int
+ sample: 4096
+type:
+ description: Algorithm used to generate the TLS/SSL private key.
+ returned: changed or success
+ type: str
+ sample: RSA
+curve:
+ description: Elliptic curve used to generate the TLS/SSL private key.
+ returned: changed or success, and I(type) is C(ECC)
+ type: str
+ sample: secp256r1
+fingerprint:
+ description:
+ - The fingerprint of the public key. Fingerprint will be generated for each C(hashlib.algorithms) available.
+ returned: changed or success
+ type: dict
+ sample:
+ md5: "84:75:71:72:8d:04:b5:6c:4d:37:6d:66:83:f5:4c:29"
+ sha1: "51:cc:7c:68:5d:eb:41:43:88:7e:1a:ae:c7:f8:24:72:ee:71:f6:10"
+ sha224: "b1:19:a6:6c:14:ac:33:1d:ed:18:50:d3:06:5c:b2:32:91:f1:f1:52:8c:cb:d5:75:e9:f5:9b:46"
+ sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7"
+ sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d"
+ sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b"
+privatekey:
+ description:
+ - The generated private key's content.
+ - Please note that if the result is not changed, the current private key will only be returned
+ if the I(return_current_key) option is set to C(true).
+ - Will be Base64-encoded if the key is in raw format.
+ returned: changed, or I(return_current_key) is C(true)
+ type: str
+'''
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_publickey.py b/ansible_collections/community/crypto/plugins/modules/openssl_publickey.py
new file mode 100644
index 00000000..da01d1fb
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_publickey.py
@@ -0,0 +1,488 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016, Yanis Guenane <yanis+ansible@guenane.org>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_publickey
+short_description: Generate an OpenSSL public key from its private key.
+description:
+ - This module allows one to (re)generate public keys from their private keys.
+ - Public keys are generated in PEM or OpenSSH format. Private keys must be OpenSSL PEM keys.
+ OpenSSH private keys are not supported, use the M(community.crypto.openssh_keypair) module to manage these.
+ - The module uses the cryptography Python library.
+requirements:
+ - cryptography >= 1.2.3 (older versions might work as well)
+ - Needs cryptography >= 1.4 if I(format) is C(OpenSSH)
+author:
+ - Yanis Guenane (@Spredzy)
+ - Felix Fontein (@felixfontein)
+extends_documentation_fragment:
+ - ansible.builtin.files
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+ safe_file_operations:
+ support: full
+options:
+ state:
+ description:
+ - Whether the public key should exist or not, taking action if the state is different from what is stated.
+ type: str
+ default: present
+ choices: [ absent, present ]
+ force:
+ description:
+ - Should the key be regenerated even it it already exists.
+ type: bool
+ default: false
+ format:
+ description:
+ - The format of the public key.
+ type: str
+ default: PEM
+ choices: [ OpenSSH, PEM ]
+ path:
+ description:
+ - Name of the file in which the generated TLS/SSL public key will be written.
+ type: path
+ required: true
+ privatekey_path:
+ description:
+ - Path to the TLS/SSL private key from which to generate the public key.
+ - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both.
+ If I(state) is C(present), one of them is required.
+ type: path
+ privatekey_content:
+ description:
+ - The content of the TLS/SSL private key from which to generate the public key.
+ - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both.
+ If I(state) is C(present), one of them is required.
+ type: str
+ version_added: '1.0.0'
+ privatekey_passphrase:
+ description:
+ - The passphrase for the private key.
+ type: str
+ backup:
+ description:
+ - Create a backup file including a timestamp so you can get the original
+ public key back if you overwrote it with a different one by accident.
+ type: bool
+ default: false
+ select_crypto_backend:
+ description:
+ - Determines which crypto backend to use.
+ - The default choice is C(auto), which tries to use C(cryptography) if available.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography ]
+ return_content:
+ description:
+ - If set to C(true), will return the (current or generated) public key's content as I(publickey).
+ type: bool
+ default: false
+ version_added: '1.0.0'
+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'''
+- name: Generate an OpenSSL public key in PEM format
+ community.crypto.openssl_publickey:
+ path: /etc/ssl/public/ansible.com.pem
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+
+- name: Generate an OpenSSL public key in PEM format from an inline key
+ community.crypto.openssl_publickey:
+ path: /etc/ssl/public/ansible.com.pem
+ privatekey_content: "{{ private_key_content }}"
+
+- name: Generate an OpenSSL public key in OpenSSH v2 format
+ community.crypto.openssl_publickey:
+ path: /etc/ssl/public/ansible.com.pem
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ format: OpenSSH
+
+- name: Generate an OpenSSL public key with a passphrase protected private key
+ community.crypto.openssl_publickey:
+ path: /etc/ssl/public/ansible.com.pem
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ privatekey_passphrase: ansible
+
+- name: Force regenerate an OpenSSL public key if it already exists
+ community.crypto.openssl_publickey:
+ path: /etc/ssl/public/ansible.com.pem
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ force: true
+
+- name: Remove an OpenSSL public key
+ community.crypto.openssl_publickey:
+ path: /etc/ssl/public/ansible.com.pem
+ state: absent
+'''
+
+RETURN = r'''
+privatekey:
+ description:
+ - Path to the TLS/SSL private key the public key was generated from.
+ - Will be C(none) if the private key has been provided in I(privatekey_content).
+ returned: changed or success
+ type: str
+ sample: /etc/ssl/private/ansible.com.pem
+format:
+ description: The format of the public key (PEM, OpenSSH, ...).
+ returned: changed or success
+ type: str
+ sample: PEM
+filename:
+ description: Path to the generated TLS/SSL public key file.
+ returned: changed or success
+ type: str
+ sample: /etc/ssl/public/ansible.com.pem
+fingerprint:
+ description:
+ - The fingerprint of the public key. Fingerprint will be generated for each hashlib.algorithms available.
+ returned: changed or success
+ type: dict
+ sample:
+ md5: "84:75:71:72:8d:04:b5:6c:4d:37:6d:66:83:f5:4c:29"
+ sha1: "51:cc:7c:68:5d:eb:41:43:88:7e:1a:ae:c7:f8:24:72:ee:71:f6:10"
+ sha224: "b1:19:a6:6c:14:ac:33:1d:ed:18:50:d3:06:5c:b2:32:91:f1:f1:52:8c:cb:d5:75:e9:f5:9b:46"
+ sha256: "41:ab:c7:cb:d5:5f:30:60:46:99:ac:d4:00:70:cf:a1:76:4f:24:5d:10:24:57:5d:51:6e:09:97:df:2f:de:c7"
+ sha384: "85:39:50:4e:de:d9:19:33:40:70:ae:10:ab:59:24:19:51:c3:a2:e4:0b:1c:b1:6e:dd:b3:0c:d9:9e:6a:46:af:da:18:f8:ef:ae:2e:c0:9a:75:2c:9b:b3:0f:3a:5f:3d"
+ sha512: "fd:ed:5e:39:48:5f:9f:fe:7f:25:06:3f:79:08:cd:ee:a5:e7:b3:3d:13:82:87:1f:84:e1:f5:c7:28:77:53:94:86:56:38:69:f0:d9:35:22:01:1e:a6:60:...:0f:9b"
+backup_file:
+ description: Name of backup file created.
+ returned: changed and if I(backup) is C(true)
+ type: str
+ sample: /path/to/publickey.pem.2019-03-09@11:22~
+publickey:
+ description: The (current or generated) public key's content.
+ returned: if I(state) is C(present) and I(return_content) is C(true)
+ type: str
+ version_added: '1.0.0'
+'''
+
+import os
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.io import (
+ load_file_if_exists,
+ write_file,
+)
+
+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,
+ get_fingerprint,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
+ PublicKeyParseError,
+ get_publickey_info,
+)
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
+MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = '1.4'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives import serialization as crypto_serialization
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+
+class PublicKeyError(OpenSSLObjectError):
+ pass
+
+
+class PublicKey(OpenSSLObject):
+
+ def __init__(self, module, backend):
+ super(PublicKey, self).__init__(
+ module.params['path'],
+ module.params['state'],
+ module.params['force'],
+ module.check_mode
+ )
+ self.module = module
+ self.format = module.params['format']
+ self.privatekey_path = module.params['privatekey_path']
+ self.privatekey_content = module.params['privatekey_content']
+ if self.privatekey_content is not None:
+ self.privatekey_content = self.privatekey_content.encode('utf-8')
+ self.privatekey_passphrase = module.params['privatekey_passphrase']
+ self.privatekey = None
+ self.publickey_bytes = None
+ self.return_content = module.params['return_content']
+ self.fingerprint = {}
+ self.backend = backend
+
+ self.backup = module.params['backup']
+ self.backup_file = None
+
+ self.diff_before = self._get_info(None)
+ self.diff_after = self._get_info(None)
+
+ def _get_info(self, data):
+ if data is None:
+ return dict()
+ result = dict(can_parse_key=False)
+ try:
+ result.update(get_publickey_info(
+ self.module, self.backend, content=data, prefer_one_fingerprint=True))
+ result['can_parse_key'] = True
+ except PublicKeyParseError as exc:
+ result.update(exc.result)
+ except Exception as exc:
+ pass
+ return result
+
+ def _create_publickey(self, module):
+ self.privatekey = load_privatekey(
+ path=self.privatekey_path,
+ content=self.privatekey_content,
+ passphrase=self.privatekey_passphrase,
+ backend=self.backend
+ )
+ if self.backend == 'cryptography':
+ if self.format == 'OpenSSH':
+ return self.privatekey.public_key().public_bytes(
+ crypto_serialization.Encoding.OpenSSH,
+ crypto_serialization.PublicFormat.OpenSSH
+ )
+ else:
+ return self.privatekey.public_key().public_bytes(
+ crypto_serialization.Encoding.PEM,
+ crypto_serialization.PublicFormat.SubjectPublicKeyInfo
+ )
+
+ def generate(self, module):
+ """Generate the public key."""
+
+ if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
+ raise PublicKeyError(
+ 'The private key %s does not exist' % self.privatekey_path
+ )
+
+ if not self.check(module, perms_required=False) or self.force:
+ try:
+ publickey_content = self._create_publickey(module)
+ self.diff_after = self._get_info(publickey_content)
+ if self.return_content:
+ self.publickey_bytes = publickey_content
+
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ write_file(module, publickey_content)
+
+ self.changed = True
+ except OpenSSLBadPassphraseError as exc:
+ raise PublicKeyError(exc)
+ except (IOError, OSError) as exc:
+ raise PublicKeyError(exc)
+
+ self.fingerprint = get_fingerprint(
+ path=self.privatekey_path,
+ content=self.privatekey_content,
+ passphrase=self.privatekey_passphrase,
+ backend=self.backend,
+ )
+ file_args = module.load_file_common_arguments(module.params)
+ if module.check_file_absent_if_check_mode(file_args['path']):
+ self.changed = True
+ elif module.set_fs_attributes_if_different(file_args, False):
+ self.changed = True
+
+ def check(self, module, perms_required=True):
+ """Ensure the resource is in its desired state."""
+
+ state_and_perms = super(PublicKey, self).check(module, perms_required)
+
+ def _check_privatekey():
+ if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
+ return False
+
+ try:
+ with open(self.path, 'rb') as public_key_fh:
+ publickey_content = public_key_fh.read()
+ self.diff_before = self.diff_after = self._get_info(publickey_content)
+ if self.return_content:
+ self.publickey_bytes = publickey_content
+ if self.backend == 'cryptography':
+ if self.format == 'OpenSSH':
+ # Read and dump public key. Makes sure that the comment is stripped off.
+ current_publickey = crypto_serialization.load_ssh_public_key(publickey_content, backend=default_backend())
+ publickey_content = current_publickey.public_bytes(
+ crypto_serialization.Encoding.OpenSSH,
+ crypto_serialization.PublicFormat.OpenSSH
+ )
+ else:
+ current_publickey = crypto_serialization.load_pem_public_key(publickey_content, backend=default_backend())
+ publickey_content = current_publickey.public_bytes(
+ crypto_serialization.Encoding.PEM,
+ crypto_serialization.PublicFormat.SubjectPublicKeyInfo
+ )
+ except Exception as dummy:
+ return False
+
+ try:
+ desired_publickey = self._create_publickey(module)
+ except OpenSSLBadPassphraseError as exc:
+ raise PublicKeyError(exc)
+
+ return publickey_content == desired_publickey
+
+ if not state_and_perms:
+ return state_and_perms
+
+ return _check_privatekey()
+
+ def remove(self, module):
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ super(PublicKey, self).remove(module)
+
+ def dump(self):
+ """Serialize the object into a dictionary."""
+
+ result = {
+ 'privatekey': self.privatekey_path,
+ 'filename': self.path,
+ 'format': self.format,
+ 'changed': self.changed,
+ 'fingerprint': self.fingerprint,
+ }
+ if self.backup_file:
+ result['backup_file'] = self.backup_file
+ if self.return_content:
+ if self.publickey_bytes is None:
+ self.publickey_bytes = load_file_if_exists(self.path, ignore_errors=True)
+ result['publickey'] = self.publickey_bytes.decode('utf-8') if self.publickey_bytes else None
+
+ result['diff'] = dict(
+ before=self.diff_before,
+ after=self.diff_after,
+ )
+
+ return result
+
+
+def main():
+
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(type='str', default='present', choices=['present', 'absent']),
+ force=dict(type='bool', default=False),
+ path=dict(type='path', required=True),
+ privatekey_path=dict(type='path'),
+ privatekey_content=dict(type='str', no_log=True),
+ format=dict(type='str', default='PEM', choices=['OpenSSH', 'PEM']),
+ privatekey_passphrase=dict(type='str', no_log=True),
+ backup=dict(type='bool', default=False),
+ select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
+ return_content=dict(type='bool', default=False),
+ ),
+ supports_check_mode=True,
+ add_file_common_args=True,
+ required_if=[('state', 'present', ['privatekey_path', 'privatekey_content'], True)],
+ mutually_exclusive=(
+ ['privatekey_path', 'privatekey_content'],
+ ),
+ )
+
+ minimal_cryptography_version = MINIMAL_CRYPTOGRAPHY_VERSION
+ if module.params['format'] == 'OpenSSH':
+ minimal_cryptography_version = MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH
+
+ backend = module.params['select_crypto_backend']
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(minimal_cryptography_version)
+
+ # Decision
+ if can_use_cryptography:
+ backend = 'cryptography'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect the required Python library "
+ "cryptography (>= {0})").format(minimal_cryptography_version))
+
+ if module.params['format'] == 'OpenSSH' and backend != 'cryptography':
+ module.fail_json(msg="Format OpenSSH requires the cryptography backend.")
+
+ if backend == 'cryptography':
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(minimal_cryptography_version)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+
+ base_dir = os.path.dirname(module.params['path']) or '.'
+ if not os.path.isdir(base_dir):
+ module.fail_json(
+ name=base_dir,
+ msg="The directory '%s' does not exist or the file is not a directory" % base_dir
+ )
+
+ try:
+ public_key = PublicKey(module, backend)
+
+ if public_key.state == 'present':
+ if module.check_mode:
+ result = public_key.dump()
+ result['changed'] = module.params['force'] or not public_key.check(module)
+ module.exit_json(**result)
+
+ public_key.generate(module)
+ else:
+ if module.check_mode:
+ result = public_key.dump()
+ result['changed'] = os.path.exists(module.params['path'])
+ module.exit_json(**result)
+
+ public_key.remove(module)
+
+ result = public_key.dump()
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_publickey_info.py b/ansible_collections/community/crypto/plugins/modules/openssl_publickey_info.py
new file mode 100644
index 00000000..7b061006
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_publickey_info.py
@@ -0,0 +1,217 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2021, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_publickey_info
+short_description: Provide information for OpenSSL public keys
+description:
+ - This module allows one to query information on OpenSSL public keys.
+ - It uses the cryptography python library to interact with OpenSSL.
+version_added: 1.7.0
+requirements:
+ - cryptography >= 1.2.3
+author:
+ - Felix Fontein (@felixfontein)
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - community.crypto.attributes.info_module
+options:
+ path:
+ description:
+ - Remote absolute path where the public key file is loaded from.
+ type: path
+ content:
+ description:
+ - Content of the public key file.
+ - Either I(path) or I(content) must be specified, but not both.
+ 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.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography ]
+
+seealso:
+ - module: community.crypto.openssl_publickey
+ - module: community.crypto.openssl_privatekey_info
+ - ref: community.crypto.openssl_publickey_info filter <ansible_collections.community.crypto.openssl_publickey_info_filter>
+ # - plugin: community.crypto.openssl_publickey_info
+ # plugin_type: filter
+ description: A filter variant of this module.
+'''
+
+EXAMPLES = r'''
+- name: Generate an OpenSSL private key with the default values (4096 bits, RSA)
+ community.crypto.openssl_privatekey:
+ path: /etc/ssl/private/ansible.com.pem
+
+- name: Create public key from private key
+ community.crypto.openssl_publickey:
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ path: /etc/ssl/ansible.com.pub
+
+- name: Get information on public key
+ community.crypto.openssl_publickey_info:
+ path: /etc/ssl/ansible.com.pub
+ register: result
+
+- name: Dump information
+ ansible.builtin.debug:
+ var: result
+'''
+
+RETURN = r'''
+fingerprints:
+ description:
+ - Fingerprints of public key.
+ - For every hash algorithm available, the fingerprint is computed.
+ returned: success
+ type: dict
+ sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
+ 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
+type:
+ description:
+ - The key's type.
+ - One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
+ - Will start with C(unknown) if the key type cannot be determined.
+ returned: success
+ type: str
+ sample: RSA
+public_data:
+ description:
+ - Public key data. Depends on key type.
+ returned: success
+ type: dict
+ contains:
+ size:
+ description:
+ - Bit size of modulus (RSA) or prime number (DSA).
+ type: int
+ returned: When C(type=RSA) or C(type=DSA)
+ modulus:
+ description:
+ - The RSA key's modulus.
+ type: int
+ returned: When C(type=RSA)
+ exponent:
+ description:
+ - The RSA key's public exponent.
+ type: int
+ returned: When C(type=RSA)
+ p:
+ description:
+ - The C(p) value for DSA.
+ - This is the prime modulus upon which arithmetic takes place.
+ type: int
+ returned: When C(type=DSA)
+ q:
+ description:
+ - The C(q) value for DSA.
+ - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
+ multiplicative group of the prime field used.
+ type: int
+ returned: When C(type=DSA)
+ g:
+ description:
+ - The C(g) value for DSA.
+ - This is the element spanning the subgroup of the multiplicative group of the prime field used.
+ type: int
+ returned: When C(type=DSA)
+ curve:
+ description:
+ - The curve's name for ECC.
+ type: str
+ returned: When C(type=ECC)
+ exponent_size:
+ description:
+ - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
+ type: int
+ returned: When C(type=ECC)
+ x:
+ description:
+ - The C(x) coordinate for the public point on the elliptic curve.
+ type: int
+ returned: When C(type=ECC)
+ y:
+ description:
+ - For C(type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
+ - For C(type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
+ type: int
+ returned: When C(type=DSA) or C(type=ECC)
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.publickey_info import (
+ PublicKeyParseError,
+ select_backend,
+)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ path=dict(type='path'),
+ content=dict(type='str', no_log=True),
+ select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
+ ),
+ required_one_of=(
+ ['path', 'content'],
+ ),
+ mutually_exclusive=(
+ ['path', 'content'],
+ ),
+ supports_check_mode=True,
+ )
+
+ result = dict(
+ can_load_key=False,
+ can_parse_key=False,
+ key_is_consistent=None,
+ )
+
+ if module.params['content'] is not None:
+ data = module.params['content'].encode('utf-8')
+ else:
+ try:
+ with open(module.params['path'], 'rb') as f:
+ data = f.read()
+ except (IOError, OSError) as e:
+ module.fail_json(msg='Error while reading public key file from disk: {0}'.format(e), **result)
+
+ backend, module_backend = select_backend(
+ module,
+ module.params['select_crypto_backend'],
+ data)
+
+ try:
+ result.update(module_backend.get_info())
+ module.exit_json(**result)
+ except PublicKeyParseError as exc:
+ result.update(exc.result)
+ module.fail_json(msg=exc.error_message, **result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_signature.py b/ansible_collections/community/crypto/plugins/modules/openssl_signature.py
new file mode 100644
index 00000000..363a0553
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_signature.py
@@ -0,0 +1,276 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019, Patrick Pichler <ppichler+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_signature
+version_added: 1.1.0
+short_description: Sign data with openssl
+description:
+ - This module allows one to sign data using a private key.
+ - The module uses the cryptography Python library.
+requirements:
+ - cryptography >= 1.4 (some key types require newer versions)
+author:
+ - Patrick Pichler (@aveexy)
+ - Markus Teufelberger (@MarkusTeufelberger)
+extends_documentation_fragment:
+ - community.crypto.attributes
+attributes:
+ check_mode:
+ support: full
+ details:
+ - This action does not modify state.
+ diff_mode:
+ support: none
+options:
+ privatekey_path:
+ description:
+ - The path to the private key to use when signing.
+ - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both.
+ type: path
+ privatekey_content:
+ description:
+ - The content of the private key to use when signing the certificate signing request.
+ - Either I(privatekey_path) or I(privatekey_content) must be specified, but not both.
+ type: str
+ privatekey_passphrase:
+ description:
+ - The passphrase for the private key.
+ - This is required if the private key is password protected.
+ type: str
+ path:
+ description:
+ - The file to sign.
+ - This file will only be read and not modified.
+ type: path
+ required: true
+ select_crypto_backend:
+ description:
+ - Determines which crypto backend to use.
+ - The default choice is C(auto), which tries to use C(cryptography) if available.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography ]
+notes:
+ - |
+ When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version:
+ RSA keys: C(cryptography) >= 1.4
+ DSA and ECDSA keys: C(cryptography) >= 1.5
+ ed448 and ed25519 keys: C(cryptography) >= 2.6
+seealso:
+ - module: community.crypto.openssl_signature_info
+ - module: community.crypto.openssl_privatekey
+'''
+
+EXAMPLES = r'''
+- name: Sign example file
+ community.crypto.openssl_signature:
+ privatekey_path: private.key
+ path: /tmp/example_file
+ register: sig
+
+- name: Verify signature of example file
+ community.crypto.openssl_signature_info:
+ certificate_path: cert.pem
+ path: /tmp/example_file
+ signature: "{{ sig.signature }}"
+ register: verify
+
+- name: Make sure the signature is valid
+ assert:
+ that:
+ - verify.valid
+'''
+
+RETURN = r'''
+signature:
+ description: Base64 encoded signature.
+ returned: success
+ type: str
+'''
+
+import os
+import traceback
+import base64
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ import cryptography.hazmat.primitives.asymmetric.padding
+ import cryptography.hazmat.primitives.hashes
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ CRYPTOGRAPHY_HAS_DSA_SIGN,
+ CRYPTOGRAPHY_HAS_EC_SIGN,
+ CRYPTOGRAPHY_HAS_ED25519_SIGN,
+ CRYPTOGRAPHY_HAS_ED448_SIGN,
+ CRYPTOGRAPHY_HAS_RSA_SIGN,
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ OpenSSLObject,
+ load_privatekey,
+)
+
+from ansible.module_utils.common.text.converters import to_native
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+
+
+class SignatureBase(OpenSSLObject):
+
+ def __init__(self, module, backend):
+ super(SignatureBase, self).__init__(
+ path=module.params['path'],
+ state='present',
+ force=False,
+ check_mode=module.check_mode
+ )
+
+ self.backend = backend
+
+ 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']
+
+ def generate(self):
+ # Empty method because OpenSSLObject wants this
+ pass
+
+ def dump(self):
+ # Empty method because OpenSSLObject wants this
+ pass
+
+
+# Implementation with using cryptography
+class SignatureCryptography(SignatureBase):
+
+ def __init__(self, module, backend):
+ super(SignatureCryptography, self).__init__(module, backend)
+
+ def run(self):
+ _padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15()
+ _hash = cryptography.hazmat.primitives.hashes.SHA256()
+
+ result = dict()
+
+ try:
+ with open(self.path, "rb") as f:
+ _in = f.read()
+
+ private_key = load_privatekey(
+ path=self.privatekey_path,
+ content=self.privatekey_content,
+ passphrase=self.privatekey_passphrase,
+ backend=self.backend,
+ )
+
+ signature = None
+
+ if CRYPTOGRAPHY_HAS_DSA_SIGN:
+ if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
+ signature = private_key.sign(_in, _hash)
+
+ if CRYPTOGRAPHY_HAS_EC_SIGN:
+ if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
+ signature = private_key.sign(_in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash))
+
+ if CRYPTOGRAPHY_HAS_ED25519_SIGN:
+ if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
+ signature = private_key.sign(_in)
+
+ if CRYPTOGRAPHY_HAS_ED448_SIGN:
+ if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
+ signature = private_key.sign(_in)
+
+ if CRYPTOGRAPHY_HAS_RSA_SIGN:
+ if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
+ signature = private_key.sign(_in, _padding, _hash)
+
+ if signature is None:
+ self.module.fail_json(
+ msg="Unsupported key type. Your cryptography version is {0}".format(CRYPTOGRAPHY_VERSION)
+ )
+
+ result['signature'] = base64.b64encode(signature)
+ return result
+
+ except Exception as e:
+ raise OpenSSLObjectError(e)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ privatekey_path=dict(type='path'),
+ privatekey_content=dict(type='str', no_log=True),
+ privatekey_passphrase=dict(type='str', no_log=True),
+ path=dict(type='path', required=True),
+ select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
+ ),
+ mutually_exclusive=(
+ ['privatekey_path', 'privatekey_content'],
+ ),
+ required_one_of=(
+ ['privatekey_path', 'privatekey_content'],
+ ),
+ supports_check_mode=True,
+ )
+
+ if not os.path.isfile(module.params['path']):
+ module.fail_json(
+ name=module.params['path'],
+ msg='The file {0} does not exist'.format(module.params['path'])
+ )
+
+ backend = module.params['select_crypto_backend']
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
+
+ # Decision
+ if can_use_cryptography:
+ backend = 'cryptography'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect the required Python library "
+ "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
+ try:
+ if backend == 'cryptography':
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+ _sign = SignatureCryptography(module, backend)
+
+ result = _sign.run()
+
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/openssl_signature_info.py b/ansible_collections/community/crypto/plugins/modules/openssl_signature_info.py
new file mode 100644
index 00000000..508a47c0
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/openssl_signature_info.py
@@ -0,0 +1,299 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019, Patrick Pichler <ppichler+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: openssl_signature_info
+version_added: 1.1.0
+short_description: Verify signatures with openssl
+description:
+ - This module allows one to verify a signature for a file by a certificate.
+ - The module uses the cryptography Python library.
+requirements:
+ - cryptography >= 1.4 (some key types require newer versions)
+author:
+ - Patrick Pichler (@aveexy)
+ - Markus Teufelberger (@MarkusTeufelberger)
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - community.crypto.attributes.info_module
+options:
+ path:
+ description:
+ - The signed file to verify.
+ - This file will only be read and not modified.
+ type: path
+ required: true
+ certificate_path:
+ description:
+ - The path to the certificate used to verify the signature.
+ - Either I(certificate_path) or I(certificate_content) must be specified, but not both.
+ type: path
+ certificate_content:
+ description:
+ - The content of the certificate used to verify the signature.
+ - Either I(certificate_path) or I(certificate_content) must be specified, but not both.
+ type: str
+ signature:
+ description: Base64 encoded signature.
+ type: str
+ required: true
+ select_crypto_backend:
+ description:
+ - Determines which crypto backend to use.
+ - The default choice is C(auto), which tries to use C(cryptography) if available.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography ]
+notes:
+ - |
+ When using the C(cryptography) backend, the following key types require at least the following C(cryptography) version:
+ RSA keys: C(cryptography) >= 1.4
+ DSA and ECDSA keys: C(cryptography) >= 1.5
+ ed448 and ed25519 keys: C(cryptography) >= 2.6
+seealso:
+ - module: community.crypto.openssl_signature
+ - module: community.crypto.x509_certificate
+'''
+
+EXAMPLES = r'''
+- name: Sign example file
+ community.crypto.openssl_signature:
+ privatekey_path: private.key
+ path: /tmp/example_file
+ register: sig
+
+- name: Verify signature of example file
+ community.crypto.openssl_signature_info:
+ certificate_path: cert.pem
+ path: /tmp/example_file
+ signature: "{{ sig.signature }}"
+ register: verify
+
+- name: Make sure the signature is valid
+ assert:
+ that:
+ - verify.valid
+'''
+
+RETURN = r'''
+valid:
+ description: C(true) means the signature was valid for the given file, C(false) means it was not.
+ returned: success
+ type: bool
+'''
+
+import os
+import traceback
+import base64
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ import cryptography.hazmat.primitives.asymmetric.padding
+ import cryptography.hazmat.primitives.hashes
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ CRYPTOGRAPHY_HAS_DSA_SIGN,
+ CRYPTOGRAPHY_HAS_EC_SIGN,
+ CRYPTOGRAPHY_HAS_ED25519_SIGN,
+ CRYPTOGRAPHY_HAS_ED448_SIGN,
+ CRYPTOGRAPHY_HAS_RSA_SIGN,
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ OpenSSLObject,
+ load_certificate,
+)
+
+from ansible.module_utils.common.text.converters import to_native
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+
+
+class SignatureInfoBase(OpenSSLObject):
+
+ def __init__(self, module, backend):
+ super(SignatureInfoBase, self).__init__(
+ path=module.params['path'],
+ state='present',
+ force=False,
+ check_mode=module.check_mode
+ )
+
+ self.backend = backend
+
+ self.signature = module.params['signature']
+ self.certificate_path = module.params['certificate_path']
+ self.certificate_content = module.params['certificate_content']
+ if self.certificate_content is not None:
+ self.certificate_content = self.certificate_content.encode('utf-8')
+
+ def generate(self):
+ # Empty method because OpenSSLObject wants this
+ pass
+
+ def dump(self):
+ # Empty method because OpenSSLObject wants this
+ pass
+
+
+# Implementation with using cryptography
+class SignatureInfoCryptography(SignatureInfoBase):
+
+ def __init__(self, module, backend):
+ super(SignatureInfoCryptography, self).__init__(module, backend)
+
+ def run(self):
+ _padding = cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15()
+ _hash = cryptography.hazmat.primitives.hashes.SHA256()
+
+ result = dict()
+
+ try:
+ with open(self.path, "rb") as f:
+ _in = f.read()
+
+ _signature = base64.b64decode(self.signature)
+ certificate = load_certificate(
+ path=self.certificate_path,
+ content=self.certificate_content,
+ backend=self.backend,
+ )
+ public_key = certificate.public_key()
+ verified = False
+ valid = False
+
+ if CRYPTOGRAPHY_HAS_DSA_SIGN:
+ try:
+ if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey):
+ public_key.verify(_signature, _in, _hash)
+ verified = True
+ valid = True
+ except cryptography.exceptions.InvalidSignature:
+ verified = True
+ valid = False
+
+ if CRYPTOGRAPHY_HAS_EC_SIGN:
+ try:
+ if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
+ public_key.verify(_signature, _in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash))
+ verified = True
+ valid = True
+ except cryptography.exceptions.InvalidSignature:
+ verified = True
+ valid = False
+
+ if CRYPTOGRAPHY_HAS_ED25519_SIGN:
+ try:
+ if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
+ public_key.verify(_signature, _in)
+ verified = True
+ valid = True
+ except cryptography.exceptions.InvalidSignature:
+ verified = True
+ valid = False
+
+ if CRYPTOGRAPHY_HAS_ED448_SIGN:
+ try:
+ if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
+ public_key.verify(_signature, _in)
+ verified = True
+ valid = True
+ except cryptography.exceptions.InvalidSignature:
+ verified = True
+ valid = False
+
+ if CRYPTOGRAPHY_HAS_RSA_SIGN:
+ try:
+ if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
+ public_key.verify(_signature, _in, _padding, _hash)
+ verified = True
+ valid = True
+ except cryptography.exceptions.InvalidSignature:
+ verified = True
+ valid = False
+
+ if not verified:
+ self.module.fail_json(
+ msg="Unsupported key type. Your cryptography version is {0}".format(CRYPTOGRAPHY_VERSION)
+ )
+ result['valid'] = valid
+ return result
+
+ except Exception as e:
+ raise OpenSSLObjectError(e)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ certificate_path=dict(type='path'),
+ certificate_content=dict(type='str'),
+ path=dict(type='path', required=True),
+ signature=dict(type='str', required=True),
+ select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
+ ),
+ mutually_exclusive=(
+ ['certificate_path', 'certificate_content'],
+ ),
+ required_one_of=(
+ ['certificate_path', 'certificate_content'],
+ ),
+ supports_check_mode=True,
+ )
+
+ if not os.path.isfile(module.params['path']):
+ module.fail_json(
+ name=module.params['path'],
+ msg='The file {0} does not exist'.format(module.params['path'])
+ )
+
+ backend = module.params['select_crypto_backend']
+ if backend == 'auto':
+ # Detection what is possible
+ can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
+
+ # Decision
+ if can_use_cryptography:
+ backend = 'cryptography'
+
+ # Success?
+ if backend == 'auto':
+ module.fail_json(msg=("Cannot detect any of the required Python libraries "
+ "cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
+ try:
+ if backend == 'cryptography':
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+ _sign = SignatureInfoCryptography(module, backend)
+
+ result = _sign.run()
+
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/x509_certificate.py b/ansible_collections/community/crypto/plugins/modules/x509_certificate.py
new file mode 100644
index 00000000..398dfabc
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/x509_certificate.py
@@ -0,0 +1,419 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: x509_certificate
+short_description: Generate and/or check OpenSSL certificates
+description:
+ - It implements a notion of provider (one of C(selfsigned), C(ownca), C(acme), and C(entrust))
+ for your certificate.
+ - "Please note that the module regenerates existing certificate if it does not 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."
+ - 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
+ old short name (or by C(ansible.builtin.openssl_certificate)), which redirects to
+ C(community.crypto.x509_certificate). When using FQCNs or when using the
+ L(collections,https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#using-collections-in-a-playbook)
+ keyword, the new name M(community.crypto.x509_certificate) should be used to avoid
+ a deprecation warning.
+author:
+ - Yanis Guenane (@Spredzy)
+ - Markus Teufelberger (@MarkusTeufelberger)
+extends_documentation_fragment:
+ - ansible.builtin.files
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+ - community.crypto.module_certificate
+ - community.crypto.module_certificate.backend_acme_documentation
+ - community.crypto.module_certificate.backend_entrust_documentation
+ - community.crypto.module_certificate.backend_ownca_documentation
+ - community.crypto.module_certificate.backend_selfsigned_documentation
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+ safe_file_operations:
+ support: full
+options:
+ state:
+ description:
+ - Whether the certificate should exist or not, taking action if the state is different from what is stated.
+ type: str
+ default: present
+ choices: [ absent, present ]
+
+ path:
+ description:
+ - Remote absolute path where the generated certificate file should be created or is already located.
+ type: path
+ required: true
+
+ provider:
+ description:
+ - Name of the provider to use to generate/retrieve the OpenSSL certificate.
+ Please see the examples on how to emulate it with
+ M(community.crypto.x509_certificate_info), M(community.crypto.openssl_csr_info),
+ M(community.crypto.openssl_privatekey_info) and M(ansible.builtin.assert).
+ - "The C(entrust) provider was added for Ansible 2.9 and requires credentials for the
+ L(Entrust Certificate Services,https://www.entrustdatacard.com/products/categories/ssl-certificates) (ECS) API."
+ - Required if I(state) is C(present).
+ type: str
+ choices: [ acme, entrust, ownca, selfsigned ]
+
+ return_content:
+ description:
+ - If set to C(true), will return the (current or generated) certificate's content as I(certificate).
+ type: bool
+ default: false
+ version_added: '1.0.0'
+
+ backup:
+ description:
+ - Create a backup file including a timestamp so you can get the original
+ certificate back if you overwrote it with a new one by accident.
+ type: bool
+ default: false
+
+ 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'
+
+seealso:
+ - module: community.crypto.x509_certificate_pipe
+'''
+
+EXAMPLES = r'''
+- name: Generate a Self Signed OpenSSL certificate
+ community.crypto.x509_certificate:
+ path: /etc/ssl/crt/ansible.com.crt
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ csr_path: /etc/ssl/csr/ansible.com.csr
+ provider: selfsigned
+
+- name: Generate an OpenSSL certificate signed with your own CA certificate
+ community.crypto.x509_certificate:
+ path: /etc/ssl/crt/ansible.com.crt
+ csr_path: /etc/ssl/csr/ansible.com.csr
+ ownca_path: /etc/ssl/crt/ansible_CA.crt
+ ownca_privatekey_path: /etc/ssl/private/ansible_CA.pem
+ provider: ownca
+
+- name: Generate a Let's Encrypt Certificate
+ community.crypto.x509_certificate:
+ path: /etc/ssl/crt/ansible.com.crt
+ csr_path: /etc/ssl/csr/ansible.com.csr
+ provider: acme
+ acme_accountkey_path: /etc/ssl/private/ansible.com.pem
+ acme_challenge_path: /etc/ssl/challenges/ansible.com/
+
+- name: Force (re-)generate a new Let's Encrypt Certificate
+ community.crypto.x509_certificate:
+ path: /etc/ssl/crt/ansible.com.crt
+ csr_path: /etc/ssl/csr/ansible.com.csr
+ provider: acme
+ acme_accountkey_path: /etc/ssl/private/ansible.com.pem
+ acme_challenge_path: /etc/ssl/challenges/ansible.com/
+ force: true
+
+- name: Generate an Entrust certificate via the Entrust Certificate Services (ECS) API
+ community.crypto.x509_certificate:
+ path: /etc/ssl/crt/ansible.com.crt
+ csr_path: /etc/ssl/csr/ansible.com.csr
+ provider: entrust
+ entrust_requester_name: Jo Doe
+ entrust_requester_email: jdoe@ansible.com
+ entrust_requester_phone: 555-555-5555
+ entrust_cert_type: STANDARD_SSL
+ entrust_api_user: apiusername
+ entrust_api_key: a^lv*32!cd9LnT
+ entrust_api_client_cert_path: /etc/ssl/entrust/ecs-client.crt
+ entrust_api_client_cert_key_path: /etc/ssl/entrust/ecs-key.crt
+ entrust_api_specification_path: /etc/ssl/entrust/api-docs/cms-api-2.1.0.yaml
+
+# The following example shows how to emulate the behavior of the removed
+# "assertonly" provider with the x509_certificate_info, openssl_csr_info,
+# openssl_privatekey_info and assert modules:
+
+- name: Get certificate information
+ community.crypto.x509_certificate_info:
+ path: /etc/ssl/crt/ansible.com.crt
+ # for valid_at, invalid_at and valid_in
+ valid_at:
+ one_day_ten_hours: "+1d10h"
+ fixed_timestamp: 20200331202428Z
+ ten_seconds: "+10"
+ register: result
+
+- name: Get CSR information
+ community.crypto.openssl_csr_info:
+ # Verifies that the CSR signature is valid; module will fail if not
+ path: /etc/ssl/csr/ansible.com.csr
+ register: result_csr
+
+- name: Get private key information
+ community.crypto.openssl_privatekey_info:
+ path: /etc/ssl/csr/ansible.com.key
+ register: result_privatekey
+
+- assert:
+ that:
+ # When private key was specified for assertonly, this was checked:
+ - result.public_key == result_privatekey.public_key
+ # When CSR was specified for assertonly, this was checked:
+ - result.public_key == result_csr.public_key
+ - result.subject_ordered == result_csr.subject_ordered
+ - result.extensions_by_oid == result_csr.extensions_by_oid
+ # signature_algorithms check
+ - "result.signature_algorithm == 'sha256WithRSAEncryption' or result.signature_algorithm == 'sha512WithRSAEncryption'"
+ # subject and subject_strict
+ - "result.subject.commonName == 'ansible.com'"
+ - "result.subject | length == 1" # the number must be the number of entries you check for
+ # issuer and issuer_strict
+ - "result.issuer.commonName == 'ansible.com'"
+ - "result.issuer | length == 1" # the number must be the number of entries you check for
+ # has_expired
+ - not result.expired
+ # version
+ - result.version == 3
+ # key_usage and key_usage_strict
+ - "'Data Encipherment' in result.key_usage"
+ - "result.key_usage | length == 1" # the number must be the number of entries you check for
+ # extended_key_usage and extended_key_usage_strict
+ - "'DVCS' in result.extended_key_usage"
+ - "result.extended_key_usage | length == 1" # the number must be the number of entries you check for
+ # subject_alt_name and subject_alt_name_strict
+ - "'dns:ansible.com' in result.subject_alt_name"
+ - "result.subject_alt_name | length == 1" # the number must be the number of entries you check for
+ # not_before and not_after
+ - "result.not_before == '20190331202428Z'"
+ - "result.not_after == '20190413202428Z'"
+ # valid_at, invalid_at and valid_in
+ - "result.valid_at.one_day_ten_hours" # for valid_at
+ - "not result.valid_at.fixed_timestamp" # for invalid_at
+ - "result.valid_at.ten_seconds" # for valid_in
+'''
+
+RETURN = r'''
+filename:
+ description: Path to the generated certificate.
+ returned: changed or success
+ type: str
+ sample: /etc/ssl/crt/www.ansible.com.crt
+backup_file:
+ description: Name of backup file created.
+ returned: changed and if I(backup) is C(true)
+ type: str
+ sample: /path/to/www.ansible.com.crt.2019-03-09@11:22~
+certificate:
+ description: The (current or generated) certificate's content.
+ returned: if I(state) is C(present) and I(return_content) is C(true)
+ type: str
+ version_added: '1.0.0'
+'''
+
+
+import os
+
+from ansible.module_utils.common.text.converters 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_acme import (
+ AcmeCertificateProvider,
+ add_acme_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.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,
+ write_file,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ OpenSSLObject,
+)
+
+
+class CertificateAbsent(OpenSSLObject):
+ def __init__(self, module):
+ super(CertificateAbsent, self).__init__(
+ module.params['path'],
+ module.params['state'],
+ module.params['force'],
+ module.check_mode
+ )
+ self.module = module
+ self.return_content = module.params['return_content']
+ self.backup = module.params['backup']
+ self.backup_file = None
+
+ def generate(self, module):
+ pass
+
+ def remove(self, module):
+ if self.backup:
+ self.backup_file = module.backup_local(self.path)
+ super(CertificateAbsent, self).remove(module)
+
+ def dump(self, check_mode=False):
+ result = {
+ 'changed': self.changed,
+ 'filename': self.path,
+ 'privatekey': self.module.params['privatekey_path'],
+ 'csr': self.module.params['csr_path']
+ }
+ if self.backup_file:
+ result['backup_file'] = self.backup_file
+ if self.return_content:
+ result['certificate'] = None
+
+ return result
+
+
+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
+ )
+ self.module = module
+ self.return_content = module.params['return_content']
+ self.backup = module.params['backup']
+ self.backup_file = None
+
+ self.module_backend = module_backend
+ self.module_backend.set_existing(load_file_if_exists(self.path, module))
+
+ def generate(self, module):
+ 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.check_file_absent_if_check_mode(file_args['path']):
+ self.changed = True
+ else:
+ 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."""
+ return super(GenericCertificate, self).check(module, perms_required) and not self.module_backend.needs_regeneration()
+
+ def dump(self, check_mode=False):
+ result = self.module_backend.dump(include_certificate=self.return_content)
+ result.update({
+ 'changed': self.changed,
+ 'filename': self.path,
+ })
+ if self.backup_file:
+ result['backup_file'] = self.backup_file
+ return result
+
+
+def main():
+ argument_spec = get_certificate_argument_spec()
+ add_acme_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,
+ supports_check_mode=True,
+ )
+
+ try:
+ if module.params['state'] == 'absent':
+ certificate = CertificateAbsent(module)
+
+ 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(
+ name=base_dir,
+ msg='The directory %s does not exist or the file is not a directory' % base_dir
+ )
+
+ provider = module.params['provider']
+ provider_map = {
+ 'acme': AcmeCertificateProvider,
+ '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/ansible_collections/community/crypto/plugins/modules/x509_certificate_info.py b/ansible_collections/community/crypto/plugins/modules/x509_certificate_info.py
new file mode 100644
index 00000000..4c7a2bc4
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/x509_certificate_info.py
@@ -0,0 +1,466 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: x509_certificate_info
+short_description: Provide information of OpenSSL X.509 certificates
+description:
+ - This module allows one to query information on OpenSSL certificates.
+ - It uses the cryptography python library to interact with OpenSSL.
+ - Note that this module was called C(openssl_certificate_info) when included directly in Ansible
+ up to version 2.9. When moved to the collection C(community.crypto), it was renamed to
+ M(community.crypto.x509_certificate_info). From Ansible 2.10 on, it can still be used by the
+ old short name (or by C(ansible.builtin.openssl_certificate_info)), which redirects to
+ C(community.crypto.x509_certificate_info). When using FQCNs or when using the
+ L(collections,https://docs.ansible.com/ansible/latest/user_guide/collections_using.html#using-collections-in-a-playbook)
+ keyword, the new name M(community.crypto.x509_certificate_info) should be used to avoid
+ a deprecation warning.
+requirements:
+ - cryptography >= 1.6
+author:
+ - Felix Fontein (@felixfontein)
+ - Yanis Guenane (@Spredzy)
+ - Markus Teufelberger (@MarkusTeufelberger)
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - community.crypto.attributes.info_module
+ - community.crypto.name_encoding
+options:
+ path:
+ description:
+ - Remote absolute path where the certificate file is loaded from.
+ - Either I(path) or I(content) must be specified, but not both.
+ type: path
+ content:
+ description:
+ - Content of the X.509 certificate in PEM format.
+ - Either I(path) or I(content) must be specified, but not both.
+ type: str
+ version_added: '1.0.0'
+ valid_at:
+ description:
+ - A dict of names mapping to time specifications. Every time specified here
+ will be checked whether the certificate is valid at this point. See the
+ C(valid_at) return value for informations on the result.
+ - Time can be specified either as relative time or as absolute timestamp.
+ - Time will always be interpreted as UTC.
+ - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ + C([w | d | h | m | s]) (for example C(+32w1d2h)), and ASN.1 TIME (in other words, pattern C(YYYYMMDDHHMMSSZ)).
+ Note that all timestamps will be treated as being in UTC.
+ type: dict
+ select_crypto_backend:
+ description:
+ - Determines which crypto backend to use.
+ - The default choice is C(auto), which tries to use C(cryptography) if available.
+ - If set to C(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
+ type: str
+ default: auto
+ choices: [ auto, cryptography ]
+
+notes:
+ - All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
+ They are all in UTC.
+seealso:
+ - module: community.crypto.x509_certificate
+ - module: community.crypto.x509_certificate_pipe
+ - ref: community.crypto.x509_certificate_info filter <ansible_collections.community.crypto.x509_certificate_info_filter>
+ # - plugin: community.crypto.x509_certificate_info
+ # plugin_type: filter
+ description: A filter variant of this module.
+'''
+
+EXAMPLES = r'''
+- name: Generate a Self Signed OpenSSL certificate
+ community.crypto.x509_certificate:
+ path: /etc/ssl/crt/ansible.com.crt
+ privatekey_path: /etc/ssl/private/ansible.com.pem
+ csr_path: /etc/ssl/csr/ansible.com.csr
+ provider: selfsigned
+
+
+# Get information on the certificate
+
+- name: Get information on generated certificate
+ community.crypto.x509_certificate_info:
+ path: /etc/ssl/crt/ansible.com.crt
+ register: result
+
+- name: Dump information
+ ansible.builtin.debug:
+ var: result
+
+
+# Check whether the certificate is valid or not valid at certain times, fail
+# if this is not the case. The first task (x509_certificate_info) collects
+# the information, and the second task (assert) validates the result and
+# makes the playbook fail in case something is not as expected.
+
+- name: Test whether that certificate is valid tomorrow and/or in three weeks
+ community.crypto.x509_certificate_info:
+ path: /etc/ssl/crt/ansible.com.crt
+ valid_at:
+ point_1: "+1d"
+ point_2: "+3w"
+ register: result
+
+- name: Validate that certificate is valid tomorrow, but not in three weeks
+ assert:
+ that:
+ - result.valid_at.point_1 # valid in one day
+ - not result.valid_at.point_2 # not valid in three weeks
+'''
+
+RETURN = r'''
+expired:
+ description: Whether the certificate is expired (in other words, C(notAfter) is in the past).
+ returned: success
+ type: bool
+basic_constraints:
+ description: Entries in the C(basic_constraints) extension, or C(none) if extension is not present.
+ returned: success
+ type: list
+ elements: str
+ sample: ["CA:TRUE", "pathlen:1"]
+basic_constraints_critical:
+ description: Whether the C(basic_constraints) extension is critical.
+ returned: success
+ type: bool
+extended_key_usage:
+ description: Entries in the C(extended_key_usage) extension, or C(none) if extension is not present.
+ returned: success
+ type: list
+ elements: str
+ sample: [Biometric Info, DVCS, Time Stamping]
+extended_key_usage_critical:
+ description: Whether the C(extended_key_usage) extension is critical.
+ returned: success
+ type: bool
+extensions_by_oid:
+ description: Returns a dictionary for every extension OID.
+ returned: success
+ type: dict
+ contains:
+ critical:
+ description: Whether the extension is critical.
+ returned: success
+ type: bool
+ value:
+ description:
+ - The Base64 encoded value (in DER format) of the extension.
+ - B(Note) that depending on the C(cryptography) version used, it is
+ not possible to extract the ASN.1 content of the extension, but only
+ to provide the re-encoded content of the extension in case it was
+ parsed by C(cryptography). This should usually result in exactly the
+ same value, except if the original extension value was malformed.
+ returned: success
+ type: str
+ sample: "MAMCAQU="
+ sample: {"1.3.6.1.5.5.7.1.24": { "critical": false, "value": "MAMCAQU="}}
+key_usage:
+ description: Entries in the C(key_usage) extension, or C(none) if extension is not present.
+ returned: success
+ type: str
+ sample: [Key Agreement, Data Encipherment]
+key_usage_critical:
+ description: Whether the C(key_usage) extension is critical.
+ returned: success
+ type: bool
+subject_alt_name:
+ description:
+ - Entries in the C(subject_alt_name) extension, or C(none) if extension is not present.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: list
+ elements: str
+ sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
+subject_alt_name_critical:
+ description: Whether the C(subject_alt_name) extension is critical.
+ returned: success
+ type: bool
+ocsp_must_staple:
+ description: C(true) if the OCSP Must Staple extension is present, C(none) otherwise.
+ returned: success
+ type: bool
+ocsp_must_staple_critical:
+ description: Whether the C(ocsp_must_staple) extension is critical.
+ returned: success
+ type: bool
+issuer:
+ description:
+ - The certificate's issuer.
+ - Note that for repeated values, only the last one will be returned.
+ returned: success
+ type: dict
+ sample: {"organizationName": "Ansible", "commonName": "ca.example.com"}
+issuer_ordered:
+ description: The certificate's issuer as an ordered list of tuples.
+ returned: success
+ type: list
+ elements: list
+ sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]]
+subject:
+ description:
+ - The certificate's subject as a dictionary.
+ - Note that for repeated values, only the last one will be returned.
+ returned: success
+ type: dict
+ sample: {"commonName": "www.example.com", "emailAddress": "test@example.com"}
+subject_ordered:
+ description: The certificate's subject as an ordered list of tuples.
+ returned: success
+ type: list
+ elements: list
+ sample: [["commonName", "www.example.com"], ["emailAddress": "test@example.com"]]
+not_after:
+ description: C(notAfter) date as ASN.1 TIME.
+ returned: success
+ type: str
+ sample: '20190413202428Z'
+not_before:
+ description: C(notBefore) date as ASN.1 TIME.
+ returned: success
+ type: str
+ sample: '20190331202428Z'
+public_key:
+ description: Certificate's public key in PEM format.
+ returned: success
+ type: str
+ sample: "-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A..."
+public_key_type:
+ description:
+ - The certificate's public key's type.
+ - One of C(RSA), C(DSA), C(ECC), C(Ed25519), C(X25519), C(Ed448), or C(X448).
+ - Will start with C(unknown) if the key type cannot be determined.
+ returned: success
+ type: str
+ version_added: 1.7.0
+ sample: RSA
+public_key_data:
+ description:
+ - Public key data. Depends on the public key's type.
+ returned: success
+ type: dict
+ version_added: 1.7.0
+ contains:
+ size:
+ description:
+ - Bit size of modulus (RSA) or prime number (DSA).
+ type: int
+ returned: When C(public_key_type=RSA) or C(public_key_type=DSA)
+ modulus:
+ description:
+ - The RSA key's modulus.
+ type: int
+ returned: When C(public_key_type=RSA)
+ exponent:
+ description:
+ - The RSA key's public exponent.
+ type: int
+ returned: When C(public_key_type=RSA)
+ p:
+ description:
+ - The C(p) value for DSA.
+ - This is the prime modulus upon which arithmetic takes place.
+ type: int
+ returned: When C(public_key_type=DSA)
+ q:
+ description:
+ - The C(q) value for DSA.
+ - This is a prime that divides C(p - 1), and at the same time the order of the subgroup of the
+ multiplicative group of the prime field used.
+ type: int
+ returned: When C(public_key_type=DSA)
+ g:
+ description:
+ - The C(g) value for DSA.
+ - This is the element spanning the subgroup of the multiplicative group of the prime field used.
+ type: int
+ returned: When C(public_key_type=DSA)
+ curve:
+ description:
+ - The curve's name for ECC.
+ type: str
+ returned: When C(public_key_type=ECC)
+ exponent_size:
+ description:
+ - The maximum number of bits of a private key. This is basically the bit size of the subgroup used.
+ type: int
+ returned: When C(public_key_type=ECC)
+ x:
+ description:
+ - The C(x) coordinate for the public point on the elliptic curve.
+ type: int
+ returned: When C(public_key_type=ECC)
+ y:
+ description:
+ - For C(public_key_type=ECC), this is the C(y) coordinate for the public point on the elliptic curve.
+ - For C(public_key_type=DSA), this is the publicly known group element whose discrete logarithm w.r.t. C(g) is the private key.
+ type: int
+ returned: When C(public_key_type=DSA) or C(public_key_type=ECC)
+public_key_fingerprints:
+ description:
+ - Fingerprints of certificate's public key.
+ - For every hash algorithm available, the fingerprint is computed.
+ returned: success
+ type: dict
+ sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
+ 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
+fingerprints:
+ description:
+ - Fingerprints of the DER-encoded form of the whole certificate.
+ - For every hash algorithm available, the fingerprint is computed.
+ returned: success
+ type: dict
+ sample: "{'sha256': 'd4:b3:aa:6d:c8:04:ce:4e:ba:f6:29:4d:92:a3:94:b0:c2:ff:bd:bf:33:63:11:43:34:0f:51:b0:95:09:2f:63',
+ 'sha512': 'f7:07:4a:f0:b0:f0:e6:8b:95:5f:f9:e6:61:0a:32:68:f1..."
+ version_added: 1.2.0
+signature_algorithm:
+ description: The signature algorithm used to sign the certificate.
+ returned: success
+ type: str
+ sample: sha256WithRSAEncryption
+serial_number:
+ description: The certificate's serial number.
+ returned: success
+ type: int
+ sample: 1234
+version:
+ description: The certificate version.
+ returned: success
+ type: int
+ sample: 3
+valid_at:
+ description: For every time stamp provided in the I(valid_at) option, a
+ boolean whether the certificate is valid at that point in time
+ or not.
+ returned: success
+ type: dict
+subject_key_identifier:
+ description:
+ - The certificate's subject key identifier.
+ - The identifier is returned in hexadecimal, with C(:) used to separate bytes.
+ - Is C(none) if the C(SubjectKeyIdentifier) extension is not present.
+ returned: success
+ type: str
+ sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
+authority_key_identifier:
+ description:
+ - The certificate's authority key identifier.
+ - The identifier is returned in hexadecimal, with C(:) used to separate bytes.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ returned: success
+ type: str
+ sample: '00:11:22:33:44:55:66:77:88:99:aa:bb:cc:dd:ee:ff:00:11:22:33'
+authority_cert_issuer:
+ description:
+ - The certificate's authority cert issuer as a list of general names.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: list
+ elements: str
+ sample: ["DNS:www.ansible.com", "IP:1.2.3.4"]
+authority_cert_serial_number:
+ description:
+ - The certificate's authority cert serial number.
+ - Is C(none) if the C(AuthorityKeyIdentifier) extension is not present.
+ returned: success
+ type: int
+ sample: 12345
+ocsp_uri:
+ description: The OCSP responder URI, if included in the certificate. Will be
+ C(none) if no OCSP responder URI is included.
+ returned: success
+ type: str
+issuer_uri:
+ description: The Issuer URI, if included in the certificate. Will be
+ C(none) if no issuer URI is included.
+ returned: success
+ type: str
+ version_added: 2.9.0
+'''
+
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.six import string_types
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.support import (
+ get_relative_time_option,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.certificate_info import (
+ select_backend,
+)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ path=dict(type='path'),
+ content=dict(type='str'),
+ valid_at=dict(type='dict'),
+ name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
+ select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
+ ),
+ required_one_of=(
+ ['path', 'content'],
+ ),
+ mutually_exclusive=(
+ ['path', 'content'],
+ ),
+ supports_check_mode=True,
+ )
+
+ if module.params['content'] is not None:
+ data = module.params['content'].encode('utf-8')
+ else:
+ try:
+ with open(module.params['path'], 'rb') as f:
+ data = f.read()
+ except (IOError, OSError) as e:
+ module.fail_json(msg='Error while reading certificate file from disk: {0}'.format(e))
+
+ backend, module_backend = select_backend(module, module.params['select_crypto_backend'], data)
+
+ valid_at = module.params['valid_at']
+ if valid_at:
+ for k, v in valid_at.items():
+ if not isinstance(v, string_types):
+ module.fail_json(
+ msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v))
+ )
+ valid_at[k] = get_relative_time_option(v, 'valid_at.{0}'.format(k))
+
+ try:
+ result = module_backend.get_info()
+
+ not_before = module_backend.get_not_before()
+ not_after = module_backend.get_not_after()
+
+ result['valid_at'] = dict()
+ if valid_at:
+ for k, v in valid_at.items():
+ result['valid_at'][k] = not_before <= v <= not_after
+
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/x509_certificate_pipe.py b/ansible_collections/community/crypto/plugins/modules/x509_certificate_pipe.py
new file mode 100644
index 00000000..440a2cdf
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/x509_certificate_pipe.py
@@ -0,0 +1,211 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2016-2017, Yanis Guenane <yanis+ansible@guenane.org>
+# Copyright (c) 2017, Markus Teufelberger <mteufelberger+ansible@mgit.at>
+# Copyright (2) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+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 does not 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)
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - 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
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+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
+'''
+
+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
+- name: Print the certificate
+ 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
+'''
+
+
+from ansible.module_utils.common.text.converters 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/ansible_collections/community/crypto/plugins/modules/x509_crl.py b/ansible_collections/community/crypto/plugins/modules/x509_crl.py
new file mode 100644
index 00000000..cb0ea24f
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/x509_crl.py
@@ -0,0 +1,914 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2019, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: x509_crl
+version_added: '1.0.0'
+short_description: Generate Certificate Revocation Lists (CRLs)
+description:
+ - This module allows one to (re)generate or update Certificate Revocation Lists (CRLs).
+ - Certificates on the revocation list can be either specified by serial number and (optionally) their issuer,
+ or as a path to a certificate file in PEM format.
+requirements:
+ - cryptography >= 1.2
+author:
+ - Felix Fontein (@felixfontein)
+extends_documentation_fragment:
+ - ansible.builtin.files
+ - community.crypto.attributes
+ - community.crypto.attributes.files
+ - community.crypto.name_encoding
+attributes:
+ check_mode:
+ support: full
+ diff_mode:
+ support: full
+ safe_file_operations:
+ support: full
+options:
+ state:
+ description:
+ - Whether the CRL file should exist or not, taking action if the state is different from what is stated.
+ type: str
+ default: present
+ choices: [ absent, present ]
+
+ mode:
+ description:
+ - Defines how to process entries of existing CRLs.
+ - If set to C(generate), makes sure that the CRL has the exact set of revoked certificates
+ as specified in I(revoked_certificates).
+ - If set to C(update), makes sure that the CRL contains the revoked certificates from
+ I(revoked_certificates), but can also contain other revoked certificates. If the CRL file
+ already exists, all entries from the existing CRL will also be included in the new CRL.
+ When using C(update), you might be interested in setting I(ignore_timestamps) to C(true).
+ type: str
+ default: generate
+ choices: [ generate, update ]
+
+ force:
+ description:
+ - Should the CRL be forced to be regenerated.
+ type: bool
+ default: false
+
+ backup:
+ description:
+ - Create a backup file including a timestamp so you can get the original
+ CRL back if you overwrote it with a new one by accident.
+ type: bool
+ default: false
+
+ path:
+ description:
+ - Remote absolute path where the generated CRL file should be created or is already located.
+ type: path
+ required: true
+
+ format:
+ description:
+ - Whether the CRL file should be in PEM or DER format.
+ - If an existing CRL file does match everything but I(format), it will be converted to the correct format
+ instead of regenerated.
+ type: str
+ choices: [pem, der]
+ default: pem
+
+ privatekey_path:
+ description:
+ - Path to the CA's private key to use when signing the CRL.
+ - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
+ type: path
+
+ privatekey_content:
+ description:
+ - The content of the CA's private key to use when signing the CRL.
+ - Either I(privatekey_path) or I(privatekey_content) must be specified if I(state) is C(present), but not both.
+ type: str
+
+ privatekey_passphrase:
+ description:
+ - The passphrase for the I(privatekey_path).
+ - This is required if the private key is password protected.
+ type: str
+
+ issuer:
+ description:
+ - Key/value pairs that will be present in the issuer name field of the CRL.
+ - If you need to specify more than one value with the same key, use a list as value.
+ - If the order of the components is important, use I(issuer_ordered).
+ - One of I(issuer) and I(issuer_ordered) is required if I(state) is C(present).
+ - Mutually exclusive with I(issuer_ordered).
+ type: dict
+ issuer_ordered:
+ description:
+ - A list of dictionaries, where every dictionary must contain one key/value pair.
+ This key/value pair will be present in the issuer name field of the CRL.
+ - If you want to specify more than one value with the same key in a row, you can
+ use a list as value.
+ - One of I(issuer) and I(issuer_ordered) is required if I(state) is C(present).
+ - Mutually exclusive with I(issuer).
+ type: list
+ elements: dict
+ version_added: 2.0.0
+
+ last_update:
+ description:
+ - The point in time from which this CRL can be trusted.
+ - Time can be specified either as relative time or as absolute timestamp.
+ - Time will always be interpreted as UTC.
+ - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ + C([w | d | h | m | s]) (for example C(+32w1d2h)).
+ - Note that if using relative time this module is NOT idempotent, except when
+ I(ignore_timestamps) is set to C(true).
+ type: str
+ default: "+0s"
+
+ next_update:
+ description:
+ - "The absolute latest point in time by which this I(issuer) is expected to have issued
+ another CRL. Many clients will treat a CRL as expired once I(next_update) occurs."
+ - Time can be specified either as relative time or as absolute timestamp.
+ - Time will always be interpreted as UTC.
+ - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ + C([w | d | h | m | s]) (for example C(+32w1d2h)).
+ - Note that if using relative time this module is NOT idempotent, except when
+ I(ignore_timestamps) is set to C(true).
+ - Required if I(state) is C(present).
+ type: str
+
+ digest:
+ description:
+ - Digest algorithm to be used when signing the CRL.
+ type: str
+ default: sha256
+
+ revoked_certificates:
+ description:
+ - List of certificates to be revoked.
+ - Required if I(state) is C(present).
+ type: list
+ elements: dict
+ suboptions:
+ path:
+ description:
+ - Path to a certificate in PEM format.
+ - The serial number and issuer will be extracted from the certificate.
+ - Mutually exclusive with I(content) and I(serial_number). One of these three options
+ must be specified.
+ type: path
+ content:
+ description:
+ - Content of a certificate in PEM format.
+ - The serial number and issuer will be extracted from the certificate.
+ - Mutually exclusive with I(path) and I(serial_number). One of these three options
+ must be specified.
+ type: str
+ serial_number:
+ description:
+ - Serial number of the certificate.
+ - Mutually exclusive with I(path) and I(content). One of these three options must
+ be specified.
+ type: int
+ revocation_date:
+ description:
+ - The point in time the certificate was revoked.
+ - Time can be specified either as relative time or as absolute timestamp.
+ - Time will always be interpreted as UTC.
+ - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ + C([w | d | h | m | s]) (for example C(+32w1d2h)).
+ - Note that if using relative time this module is NOT idempotent, except when
+ I(ignore_timestamps) is set to C(true).
+ type: str
+ default: "+0s"
+ issuer:
+ description:
+ - The certificate's issuer.
+ - "Example: C(DNS:ca.example.org)"
+ type: list
+ elements: str
+ issuer_critical:
+ description:
+ - Whether the certificate issuer extension should be critical.
+ type: bool
+ default: false
+ reason:
+ description:
+ - The value for the revocation reason extension.
+ type: str
+ choices:
+ - unspecified
+ - key_compromise
+ - ca_compromise
+ - affiliation_changed
+ - superseded
+ - cessation_of_operation
+ - certificate_hold
+ - privilege_withdrawn
+ - aa_compromise
+ - remove_from_crl
+ reason_critical:
+ description:
+ - Whether the revocation reason extension should be critical.
+ type: bool
+ default: false
+ invalidity_date:
+ description:
+ - The point in time it was known/suspected that the private key was compromised
+ or that the certificate otherwise became invalid.
+ - Time can be specified either as relative time or as absolute timestamp.
+ - Time will always be interpreted as UTC.
+ - Valid format is C([+-]timespec | ASN.1 TIME) where timespec can be an integer
+ + C([w | d | h | m | s]) (for example C(+32w1d2h)).
+ - Note that if using relative time this module is NOT idempotent. This will NOT
+ change when I(ignore_timestamps) is set to C(true).
+ type: str
+ invalidity_date_critical:
+ description:
+ - Whether the invalidity date extension should be critical.
+ type: bool
+ default: false
+
+ ignore_timestamps:
+ description:
+ - Whether the timestamps I(last_update), I(next_update) and I(revocation_date) (in
+ I(revoked_certificates)) should be ignored for idempotency checks. The timestamp
+ I(invalidity_date) in I(revoked_certificates) will never be ignored.
+ - Use this in combination with relative timestamps for these values to get idempotency.
+ type: bool
+ default: false
+
+ return_content:
+ description:
+ - If set to C(true), will return the (current or generated) CRL's content as I(crl).
+ type: bool
+ default: false
+
+notes:
+ - All ASN.1 TIME values should be specified following the YYYYMMDDHHMMSSZ pattern.
+ - Date specified should be UTC. Minutes and seconds are mandatory.
+'''
+
+EXAMPLES = r'''
+- name: Generate a CRL
+ community.crypto.x509_crl:
+ path: /etc/ssl/my-ca.crl
+ privatekey_path: /etc/ssl/private/my-ca.pem
+ issuer:
+ CN: My CA
+ last_update: "+0s"
+ next_update: "+7d"
+ revoked_certificates:
+ - serial_number: 1234
+ revocation_date: 20190331202428Z
+ issuer:
+ CN: My CA
+ - serial_number: 2345
+ revocation_date: 20191013152910Z
+ reason: affiliation_changed
+ invalidity_date: 20191001000000Z
+ - path: /etc/ssl/crt/revoked-cert.pem
+ revocation_date: 20191010010203Z
+'''
+
+RETURN = r'''
+filename:
+ description: Path to the generated CRL.
+ returned: changed or success
+ type: str
+ sample: /path/to/my-ca.crl
+backup_file:
+ description: Name of backup file created.
+ returned: changed and if I(backup) is C(true)
+ type: str
+ sample: /path/to/my-ca.crl.2019-03-09@11:22~
+privatekey:
+ description: Path to the private CA key.
+ returned: changed or success
+ type: str
+ sample: /path/to/my-ca.pem
+format:
+ description:
+ - Whether the CRL is in PEM format (C(pem)) or in DER format (C(der)).
+ returned: success
+ type: str
+ sample: pem
+issuer:
+ description:
+ - The CRL's issuer.
+ - Note that for repeated values, only the last one will be returned.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: dict
+ sample: {"organizationName": "Ansible", "commonName": "ca.example.com"}
+issuer_ordered:
+ description: The CRL's issuer as an ordered list of tuples.
+ returned: success
+ type: list
+ elements: list
+ sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]]
+last_update:
+ description: The point in time from which this CRL can be trusted as ASN.1 TIME.
+ returned: success
+ type: str
+ sample: 20190413202428Z
+next_update:
+ description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME.
+ returned: success
+ type: str
+ sample: 20190413202428Z
+digest:
+ description: The signature algorithm used to sign the CRL.
+ returned: success
+ type: str
+ sample: sha256WithRSAEncryption
+revoked_certificates:
+ description: List of certificates to be revoked.
+ returned: success
+ type: list
+ elements: dict
+ contains:
+ serial_number:
+ description: Serial number of the certificate.
+ type: int
+ sample: 1234
+ revocation_date:
+ description: The point in time the certificate was revoked as ASN.1 TIME.
+ type: str
+ sample: 20190413202428Z
+ issuer:
+ description:
+ - The certificate's issuer.
+ - See I(name_encoding) for how IDNs are handled.
+ type: list
+ elements: str
+ sample: ["DNS:ca.example.org"]
+ issuer_critical:
+ description: Whether the certificate issuer extension is critical.
+ type: bool
+ sample: false
+ reason:
+ description:
+ - The value for the revocation reason extension.
+ - One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded),
+ C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and
+ C(remove_from_crl).
+ type: str
+ sample: key_compromise
+ reason_critical:
+ description: Whether the revocation reason extension is critical.
+ type: bool
+ sample: false
+ invalidity_date:
+ description: |
+ The point in time it was known/suspected that the private key was compromised
+ or that the certificate otherwise became invalid as ASN.1 TIME.
+ type: str
+ sample: 20190413202428Z
+ invalidity_date_critical:
+ description: Whether the invalidity date extension is critical.
+ type: bool
+ sample: false
+crl:
+ description:
+ - The (current or generated) CRL's content.
+ - Will be the CRL itself if I(format) is C(pem), and Base64 of the
+ CRL if I(format) is C(der).
+ returned: if I(state) is C(present) and I(return_content) is C(true)
+ type: str
+'''
+
+
+import base64
+import os
+import traceback
+
+from ansible.module_utils.basic import AnsibleModule, missing_required_lib
+from ansible.module_utils.common.text.converters import to_native, to_text
+
+from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion
+
+from ansible_collections.community.crypto.plugins.module_utils.io import (
+ write_file,
+)
+
+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,
+ parse_name_field,
+ parse_ordered_name_field,
+ get_relative_time_option,
+ select_message_digest,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_support import (
+ cryptography_decode_name,
+ cryptography_get_name,
+ cryptography_key_needs_digest_for_signing,
+ cryptography_name_to_oid,
+ cryptography_oid_to_name,
+ cryptography_serial_number_of_cert,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.cryptography_crl import (
+ REVOCATION_REASON_MAP,
+ TIMESTAMP_FORMAT,
+ cryptography_decode_revoked_certificate,
+ cryptography_dump_revoked,
+ cryptography_get_signature_algorithm_oid_from_crl,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
+ identify_pem_format,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import (
+ get_crl_info,
+)
+
+MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
+
+CRYPTOGRAPHY_IMP_ERR = None
+try:
+ import cryptography
+ from cryptography import x509
+ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives.serialization import Encoding
+ from cryptography.x509 import (
+ CertificateRevocationListBuilder,
+ RevokedCertificateBuilder,
+ NameAttribute,
+ Name,
+ )
+ CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
+except ImportError:
+ CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
+ CRYPTOGRAPHY_FOUND = False
+else:
+ CRYPTOGRAPHY_FOUND = True
+
+
+class CRLError(OpenSSLObjectError):
+ pass
+
+
+class CRL(OpenSSLObject):
+
+ def __init__(self, module):
+ super(CRL, self).__init__(
+ module.params['path'],
+ module.params['state'],
+ module.params['force'],
+ module.check_mode
+ )
+
+ self.format = module.params['format']
+
+ self.update = module.params['mode'] == 'update'
+ self.ignore_timestamps = module.params['ignore_timestamps']
+ self.return_content = module.params['return_content']
+ self.name_encoding = module.params['name_encoding']
+ self.crl_content = None
+
+ self.privatekey_path = module.params['privatekey_path']
+ self.privatekey_content = module.params['privatekey_content']
+ if self.privatekey_content is not None:
+ self.privatekey_content = self.privatekey_content.encode('utf-8')
+ self.privatekey_passphrase = module.params['privatekey_passphrase']
+
+ try:
+ if module.params['issuer_ordered']:
+ self.issuer_ordered = True
+ self.issuer = parse_ordered_name_field(module.params['issuer_ordered'], 'issuer_ordered')
+ else:
+ self.issuer_ordered = False
+ self.issuer = parse_name_field(module.params['issuer'], 'issuer')
+ except (TypeError, ValueError) as exc:
+ module.fail_json(msg=to_native(exc))
+
+ self.last_update = get_relative_time_option(module.params['last_update'], 'last_update')
+ self.next_update = get_relative_time_option(module.params['next_update'], 'next_update')
+
+ self.digest = select_message_digest(module.params['digest'])
+ if self.digest is None:
+ raise CRLError('The digest "{0}" is not supported'.format(module.params['digest']))
+
+ self.revoked_certificates = []
+ for i, rc in enumerate(module.params['revoked_certificates']):
+ result = {
+ 'serial_number': None,
+ 'revocation_date': None,
+ 'issuer': None,
+ 'issuer_critical': False,
+ 'reason': None,
+ 'reason_critical': False,
+ 'invalidity_date': None,
+ 'invalidity_date_critical': False,
+ }
+ path_prefix = 'revoked_certificates[{0}].'.format(i)
+ if rc['path'] is not None or rc['content'] is not None:
+ # Load certificate from file or content
+ try:
+ if rc['content'] is not None:
+ rc['content'] = rc['content'].encode('utf-8')
+ cert = load_certificate(rc['path'], content=rc['content'], backend='cryptography')
+ result['serial_number'] = cryptography_serial_number_of_cert(cert)
+ except OpenSSLObjectError as e:
+ if rc['content'] is not None:
+ module.fail_json(
+ msg='Cannot parse certificate from {0}content: {1}'.format(path_prefix, to_native(e))
+ )
+ else:
+ module.fail_json(
+ msg='Cannot read certificate "{1}" from {0}path: {2}'.format(path_prefix, rc['path'], to_native(e))
+ )
+ else:
+ # Specify serial_number (and potentially issuer) directly
+ result['serial_number'] = rc['serial_number']
+ # All other options
+ if rc['issuer']:
+ result['issuer'] = [cryptography_get_name(issuer, 'issuer') for issuer in rc['issuer']]
+ result['issuer_critical'] = rc['issuer_critical']
+ result['revocation_date'] = get_relative_time_option(
+ rc['revocation_date'],
+ path_prefix + 'revocation_date'
+ )
+ if rc['reason']:
+ result['reason'] = REVOCATION_REASON_MAP[rc['reason']]
+ result['reason_critical'] = rc['reason_critical']
+ if rc['invalidity_date']:
+ result['invalidity_date'] = get_relative_time_option(
+ rc['invalidity_date'],
+ path_prefix + 'invalidity_date'
+ )
+ result['invalidity_date_critical'] = rc['invalidity_date_critical']
+ self.revoked_certificates.append(result)
+
+ self.module = module
+
+ self.backup = module.params['backup']
+ self.backup_file = None
+
+ try:
+ self.privatekey = load_privatekey(
+ path=self.privatekey_path,
+ content=self.privatekey_content,
+ passphrase=self.privatekey_passphrase,
+ backend='cryptography'
+ )
+ except OpenSSLBadPassphraseError as exc:
+ raise CRLError(exc)
+
+ self.crl = None
+ try:
+ with open(self.path, 'rb') as f:
+ data = f.read()
+ self.actual_format = 'pem' if identify_pem_format(data) else 'der'
+ if self.actual_format == 'pem':
+ self.crl = x509.load_pem_x509_crl(data, default_backend())
+ if self.return_content:
+ self.crl_content = data
+ else:
+ self.crl = x509.load_der_x509_crl(data, default_backend())
+ if self.return_content:
+ self.crl_content = base64.b64encode(data)
+ except Exception as dummy:
+ self.crl_content = None
+ self.actual_format = self.format
+ data = None
+
+ self.diff_after = self.diff_before = self._get_info(data)
+
+ def _get_info(self, data):
+ if data is None:
+ return dict()
+ try:
+ result = get_crl_info(self.module, data)
+ result['can_parse_crl'] = True
+ return result
+ except Exception as exc:
+ return dict(can_parse_crl=False)
+
+ def remove(self):
+ if self.backup:
+ self.backup_file = self.module.backup_local(self.path)
+ super(CRL, self).remove(self.module)
+
+ def _compress_entry(self, entry):
+ issuer = None
+ if entry['issuer'] is not None:
+ # Normalize to IDNA. If this is used-provided, it was already converted to
+ # IDNA (by cryptography_get_name) and thus the `idna` library is present.
+ # If this is coming from cryptography and isn't already in IDNA (i.e. ascii),
+ # cryptography < 2.1 must be in use, which depends on `idna`. So this should
+ # not require `idna` except if it was already used by code earlier during
+ # this invocation.
+ issuer = tuple(cryptography_decode_name(issuer, idn_rewrite='idna') for issuer in entry['issuer'])
+ if self.ignore_timestamps:
+ # Throw out revocation_date
+ return (
+ entry['serial_number'],
+ issuer,
+ entry['issuer_critical'],
+ entry['reason'],
+ entry['reason_critical'],
+ entry['invalidity_date'],
+ entry['invalidity_date_critical'],
+ )
+ else:
+ return (
+ entry['serial_number'],
+ entry['revocation_date'],
+ issuer,
+ entry['issuer_critical'],
+ entry['reason'],
+ entry['reason_critical'],
+ entry['invalidity_date'],
+ entry['invalidity_date_critical'],
+ )
+
+ def check(self, module, perms_required=True, ignore_conversion=True):
+ """Ensure the resource is in its desired state."""
+
+ state_and_perms = super(CRL, self).check(self.module, perms_required)
+
+ if not state_and_perms:
+ return False
+
+ if self.crl is None:
+ return False
+
+ if self.last_update != self.crl.last_update and not self.ignore_timestamps:
+ return False
+ if self.next_update != self.crl.next_update and not self.ignore_timestamps:
+ return False
+ if cryptography_key_needs_digest_for_signing(self.privatekey):
+ if self.crl.signature_hash_algorithm is None or self.digest.name != self.crl.signature_hash_algorithm.name:
+ return False
+ else:
+ if self.crl.signature_hash_algorithm is not None:
+ return False
+
+ want_issuer = [(cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer]
+ is_issuer = [(sub.oid, sub.value) for sub in self.crl.issuer]
+ if not self.issuer_ordered:
+ want_issuer = set(want_issuer)
+ is_issuer = set(is_issuer)
+ if want_issuer != is_issuer:
+ return False
+
+ old_entries = [self._compress_entry(cryptography_decode_revoked_certificate(cert)) for cert in self.crl]
+ new_entries = [self._compress_entry(cert) for cert in self.revoked_certificates]
+ if self.update:
+ # We do not simply use a set so that duplicate entries are treated correctly
+ for entry in new_entries:
+ try:
+ old_entries.remove(entry)
+ except ValueError:
+ return False
+ else:
+ if old_entries != new_entries:
+ return False
+
+ if self.format != self.actual_format and not ignore_conversion:
+ return False
+
+ return True
+
+ def _generate_crl(self):
+ backend = default_backend()
+ crl = CertificateRevocationListBuilder()
+
+ try:
+ crl = crl.issuer_name(Name([
+ NameAttribute(cryptography_name_to_oid(entry[0]), to_text(entry[1]))
+ for entry in self.issuer
+ ]))
+ except ValueError as e:
+ raise CRLError(e)
+
+ crl = crl.last_update(self.last_update)
+ crl = crl.next_update(self.next_update)
+
+ if self.update and self.crl:
+ new_entries = set([self._compress_entry(entry) for entry in self.revoked_certificates])
+ for entry in self.crl:
+ decoded_entry = self._compress_entry(cryptography_decode_revoked_certificate(entry))
+ if decoded_entry not in new_entries:
+ crl = crl.add_revoked_certificate(entry)
+ for entry in self.revoked_certificates:
+ revoked_cert = RevokedCertificateBuilder()
+ revoked_cert = revoked_cert.serial_number(entry['serial_number'])
+ revoked_cert = revoked_cert.revocation_date(entry['revocation_date'])
+ if entry['issuer'] is not None:
+ revoked_cert = revoked_cert.add_extension(
+ x509.CertificateIssuer(entry['issuer']),
+ entry['issuer_critical']
+ )
+ if entry['reason'] is not None:
+ revoked_cert = revoked_cert.add_extension(
+ x509.CRLReason(entry['reason']),
+ entry['reason_critical']
+ )
+ if entry['invalidity_date'] is not None:
+ revoked_cert = revoked_cert.add_extension(
+ x509.InvalidityDate(entry['invalidity_date']),
+ entry['invalidity_date_critical']
+ )
+ crl = crl.add_revoked_certificate(revoked_cert.build(backend))
+
+ digest = None
+ if cryptography_key_needs_digest_for_signing(self.privatekey):
+ digest = self.digest
+ self.crl = crl.sign(self.privatekey, digest, backend=backend)
+ if self.format == 'pem':
+ return self.crl.public_bytes(Encoding.PEM)
+ else:
+ return self.crl.public_bytes(Encoding.DER)
+
+ def generate(self):
+ result = None
+ if not self.check(self.module, perms_required=False, ignore_conversion=True) or self.force:
+ result = self._generate_crl()
+ elif not self.check(self.module, perms_required=False, ignore_conversion=False) and self.crl:
+ if self.format == 'pem':
+ result = self.crl.public_bytes(Encoding.PEM)
+ else:
+ result = self.crl.public_bytes(Encoding.DER)
+
+ if result is not None:
+ self.diff_after = self._get_info(result)
+ if self.return_content:
+ if self.format == 'pem':
+ self.crl_content = result
+ else:
+ self.crl_content = base64.b64encode(result)
+ if self.backup:
+ self.backup_file = self.module.backup_local(self.path)
+ write_file(self.module, result)
+ self.changed = True
+
+ file_args = self.module.load_file_common_arguments(self.module.params)
+ if self.module.check_file_absent_if_check_mode(file_args['path']):
+ self.changed = True
+ elif self.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,
+ 'format': self.format,
+ 'last_update': None,
+ 'next_update': None,
+ 'digest': None,
+ 'issuer_ordered': None,
+ 'issuer': None,
+ 'revoked_certificates': [],
+ }
+ if self.backup_file:
+ result['backup_file'] = self.backup_file
+
+ if check_mode:
+ result['last_update'] = self.last_update.strftime(TIMESTAMP_FORMAT)
+ result['next_update'] = self.next_update.strftime(TIMESTAMP_FORMAT)
+ # result['digest'] = cryptography_oid_to_name(self.crl.signature_algorithm_oid)
+ result['digest'] = self.module.params['digest']
+ result['issuer_ordered'] = self.issuer
+ result['issuer'] = {}
+ for k, v in self.issuer:
+ result['issuer'][k] = v
+ result['revoked_certificates'] = []
+ for entry in self.revoked_certificates:
+ result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding))
+ elif self.crl:
+ result['last_update'] = self.crl.last_update.strftime(TIMESTAMP_FORMAT)
+ result['next_update'] = self.crl.next_update.strftime(TIMESTAMP_FORMAT)
+ result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl))
+ issuer = []
+ for attribute in self.crl.issuer:
+ issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value])
+ result['issuer_ordered'] = issuer
+ result['issuer'] = {}
+ for k, v in issuer:
+ result['issuer'][k] = v
+ result['revoked_certificates'] = []
+ for cert in self.crl:
+ entry = cryptography_decode_revoked_certificate(cert)
+ result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding))
+
+ if self.return_content:
+ result['crl'] = self.crl_content
+
+ result['diff'] = dict(
+ before=self.diff_before,
+ after=self.diff_after,
+ )
+ return result
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ state=dict(type='str', default='present', choices=['present', 'absent']),
+ mode=dict(type='str', default='generate', choices=['generate', 'update']),
+ force=dict(type='bool', default=False),
+ backup=dict(type='bool', default=False),
+ path=dict(type='path', required=True),
+ format=dict(type='str', default='pem', choices=['pem', 'der']),
+ privatekey_path=dict(type='path'),
+ privatekey_content=dict(type='str', no_log=True),
+ privatekey_passphrase=dict(type='str', no_log=True),
+ issuer=dict(type='dict'),
+ issuer_ordered=dict(type='list', elements='dict'),
+ last_update=dict(type='str', default='+0s'),
+ next_update=dict(type='str'),
+ digest=dict(type='str', default='sha256'),
+ ignore_timestamps=dict(type='bool', default=False),
+ return_content=dict(type='bool', default=False),
+ revoked_certificates=dict(
+ type='list',
+ elements='dict',
+ options=dict(
+ path=dict(type='path'),
+ content=dict(type='str'),
+ serial_number=dict(type='int'),
+ revocation_date=dict(type='str', default='+0s'),
+ issuer=dict(type='list', elements='str'),
+ issuer_critical=dict(type='bool', default=False),
+ reason=dict(
+ type='str',
+ choices=[
+ 'unspecified', 'key_compromise', 'ca_compromise', 'affiliation_changed',
+ 'superseded', 'cessation_of_operation', 'certificate_hold',
+ 'privilege_withdrawn', 'aa_compromise', 'remove_from_crl'
+ ]
+ ),
+ reason_critical=dict(type='bool', default=False),
+ invalidity_date=dict(type='str'),
+ invalidity_date_critical=dict(type='bool', default=False),
+ ),
+ required_one_of=[['path', 'content', 'serial_number']],
+ mutually_exclusive=[['path', 'content', 'serial_number']],
+ ),
+ name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
+ ),
+ required_if=[
+ ('state', 'present', ['privatekey_path', 'privatekey_content'], True),
+ ('state', 'present', ['issuer', 'issuer_ordered'], True),
+ ('state', 'present', ['next_update', 'revoked_certificates'], False),
+ ],
+ mutually_exclusive=(
+ ['privatekey_path', 'privatekey_content'],
+ ['issuer', 'issuer_ordered'],
+ ),
+ supports_check_mode=True,
+ add_file_common_args=True,
+ )
+
+ if not CRYPTOGRAPHY_FOUND:
+ module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
+ exception=CRYPTOGRAPHY_IMP_ERR)
+
+ try:
+ crl = CRL(module)
+
+ if module.params['state'] == 'present':
+ if module.check_mode:
+ result = crl.dump(check_mode=True)
+ result['changed'] = module.params['force'] or not crl.check(module) or not crl.check(module, ignore_conversion=False)
+ module.exit_json(**result)
+
+ crl.generate()
+ else:
+ if module.check_mode:
+ result = crl.dump(check_mode=True)
+ result['changed'] = os.path.exists(module.params['path'])
+ module.exit_json(**result)
+
+ crl.remove()
+
+ result = crl.dump()
+ module.exit_json(**result)
+ except OpenSSLObjectError as exc:
+ module.fail_json(msg=to_native(exc))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/crypto/plugins/modules/x509_crl_info.py b/ansible_collections/community/crypto/plugins/modules/x509_crl_info.py
new file mode 100644
index 00000000..7b0ebcac
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/modules/x509_crl_info.py
@@ -0,0 +1,220 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+DOCUMENTATION = r'''
+---
+module: x509_crl_info
+version_added: '1.0.0'
+short_description: Retrieve information on Certificate Revocation Lists (CRLs)
+description:
+ - This module allows one to retrieve information on Certificate Revocation Lists (CRLs).
+requirements:
+ - cryptography >= 1.2
+author:
+ - Felix Fontein (@felixfontein)
+extends_documentation_fragment:
+ - community.crypto.attributes
+ - community.crypto.attributes.info_module
+ - community.crypto.name_encoding
+options:
+ path:
+ description:
+ - Remote absolute path where the generated CRL file should be created or is already located.
+ - Either I(path) or I(content) must be specified, but not both.
+ type: path
+ content:
+ description:
+ - Content of the X.509 CRL in PEM format, or Base64-encoded X.509 CRL.
+ - Either I(path) or I(content) must be specified, but not both.
+ type: str
+ list_revoked_certificates:
+ description:
+ - If set to C(false), the list of revoked certificates is not included in the result.
+ - This is useful when retrieving information on large CRL files. Enumerating all revoked
+ certificates can take some time, including serializing the result as JSON, sending it to
+ the Ansible controller, and decoding it again.
+ type: bool
+ default: true
+ version_added: 1.7.0
+
+notes:
+ - All timestamp values are provided in ASN.1 TIME format, in other words, following the C(YYYYMMDDHHMMSSZ) pattern.
+ They are all in UTC.
+seealso:
+ - module: community.crypto.x509_crl
+ - ref: community.crypto.x509_crl_info filter <ansible_collections.community.crypto.x509_crl_info_filter>
+ # - plugin: community.crypto.x509_crl_info
+ # plugin_type: filter
+ description: A filter variant of this module.
+'''
+
+EXAMPLES = r'''
+- name: Get information on CRL
+ community.crypto.x509_crl_info:
+ path: /etc/ssl/my-ca.crl
+ register: result
+
+- name: Print the information
+ ansible.builtin.debug:
+ msg: "{{ result }}"
+
+- name: Get information on CRL without list of revoked certificates
+ community.crypto.x509_crl_info:
+ path: /etc/ssl/very-large.crl
+ list_revoked_certificates: false
+ register: result
+'''
+
+RETURN = r'''
+format:
+ description:
+ - Whether the CRL is in PEM format (C(pem)) or in DER format (C(der)).
+ returned: success
+ type: str
+ sample: pem
+issuer:
+ description:
+ - The CRL's issuer.
+ - Note that for repeated values, only the last one will be returned.
+ - See I(name_encoding) for how IDNs are handled.
+ returned: success
+ type: dict
+ sample: {"organizationName": "Ansible", "commonName": "ca.example.com"}
+issuer_ordered:
+ description: The CRL's issuer as an ordered list of tuples.
+ returned: success
+ type: list
+ elements: list
+ sample: [["organizationName", "Ansible"], ["commonName": "ca.example.com"]]
+last_update:
+ description: The point in time from which this CRL can be trusted as ASN.1 TIME.
+ returned: success
+ type: str
+ sample: '20190413202428Z'
+next_update:
+ description: The point in time from which a new CRL will be issued and the client has to check for it as ASN.1 TIME.
+ returned: success
+ type: str
+ sample: '20190413202428Z'
+digest:
+ description: The signature algorithm used to sign the CRL.
+ returned: success
+ type: str
+ sample: sha256WithRSAEncryption
+revoked_certificates:
+ description: List of certificates to be revoked.
+ returned: success if I(list_revoked_certificates=true)
+ type: list
+ elements: dict
+ contains:
+ serial_number:
+ description: Serial number of the certificate.
+ type: int
+ sample: 1234
+ revocation_date:
+ description: The point in time the certificate was revoked as ASN.1 TIME.
+ type: str
+ sample: '20190413202428Z'
+ issuer:
+ description:
+ - The certificate's issuer.
+ - See I(name_encoding) for how IDNs are handled.
+ type: list
+ elements: str
+ sample: ["DNS:ca.example.org"]
+ issuer_critical:
+ description: Whether the certificate issuer extension is critical.
+ type: bool
+ sample: false
+ reason:
+ description:
+ - The value for the revocation reason extension.
+ - One of C(unspecified), C(key_compromise), C(ca_compromise), C(affiliation_changed), C(superseded),
+ C(cessation_of_operation), C(certificate_hold), C(privilege_withdrawn), C(aa_compromise), and
+ C(remove_from_crl).
+ type: str
+ sample: key_compromise
+ reason_critical:
+ description: Whether the revocation reason extension is critical.
+ type: bool
+ sample: false
+ invalidity_date:
+ description: |
+ The point in time it was known/suspected that the private key was compromised
+ or that the certificate otherwise became invalid as ASN.1 TIME.
+ type: str
+ sample: '20190413202428Z'
+ invalidity_date_critical:
+ description: Whether the invalidity date extension is critical.
+ type: bool
+ sample: false
+'''
+
+
+import base64
+import binascii
+
+from ansible.module_utils.basic import AnsibleModule
+from ansible.module_utils.common.text.converters import to_native
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.basic import (
+ OpenSSLObjectError,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
+ identify_pem_format,
+)
+
+from ansible_collections.community.crypto.plugins.module_utils.crypto.module_backends.crl_info import (
+ get_crl_info,
+)
+
+
+def main():
+ module = AnsibleModule(
+ argument_spec=dict(
+ path=dict(type='path'),
+ content=dict(type='str'),
+ list_revoked_certificates=dict(type='bool', default=True),
+ name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
+ ),
+ required_one_of=(
+ ['path', 'content'],
+ ),
+ mutually_exclusive=(
+ ['path', 'content'],
+ ),
+ supports_check_mode=True,
+ )
+
+ if module.params['content'] is None:
+ try:
+ with open(module.params['path'], 'rb') as f:
+ data = f.read()
+ except (IOError, OSError) as e:
+ module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e))
+ else:
+ data = module.params['content'].encode('utf-8')
+ if not identify_pem_format(data):
+ try:
+ data = base64.b64decode(module.params['content'])
+ except (binascii.Error, TypeError) as e:
+ module.fail_json(msg='Error while Base64 decoding content: {0}'.format(e))
+
+ try:
+ result = get_crl_info(module, data, list_revoked_certificates=module.params['list_revoked_certificates'])
+ module.exit_json(**result)
+ except OpenSSLObjectError as e:
+ module.fail_json(msg=to_native(e))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ansible_collections/community/crypto/plugins/plugin_utils/action_module.py b/ansible_collections/community/crypto/plugins/plugin_utils/action_module.py
new file mode 100644
index 00000000..3d7a77b2
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/plugin_utils/action_module.py
@@ -0,0 +1,765 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012-2013 Michael DeHaan <michael.dehaan@gmail.com>
+# Copyright (c) 2016 Toshio Kuratomi <tkuratomi@ansible.com>
+# Copyright (c) 2019 Ansible Project
+# Copyright (c) 2020 Felix Fontein <felix@fontein.de>
+# Copyright (c) 2021 Ansible Project
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# Parts taken from ansible.module_utils.basic and ansible.module_utils.common.warnings.
+
+# NOTE: THIS IS ONLY FOR ACTION PLUGINS!
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import abc
+import copy
+import traceback
+
+from ansible import constants as C
+from ansible.errors import AnsibleError
+from ansible.module_utils import six
+from ansible.module_utils.basic import AnsibleFallbackNotFound, SEQUENCETYPE, remove_values
+from ansible.module_utils.common._collections_compat import (
+ Mapping
+)
+from ansible.module_utils.common.parameters import (
+ PASS_VARS,
+ PASS_BOOLS,
+)
+from ansible.module_utils.common.validation import (
+ check_mutually_exclusive,
+ check_required_arguments,
+ check_required_by,
+ check_required_if,
+ check_required_one_of,
+ check_required_together,
+ count_terms,
+ check_type_bool,
+ check_type_bits,
+ check_type_bytes,
+ check_type_float,
+ check_type_int,
+ check_type_jsonarg,
+ check_type_list,
+ check_type_dict,
+ check_type_path,
+ check_type_raw,
+ check_type_str,
+ safe_eval,
+)
+from ansible.module_utils.common.text.formatters import (
+ lenient_lowercase,
+)
+from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
+from ansible.module_utils.six import (
+ binary_type,
+ string_types,
+ text_type,
+)
+from ansible.module_utils.common.text.converters import to_native, to_text
+from ansible.plugins.action import ActionBase
+
+
+try:
+ # For ansible-core 2.11, we can use the ArgumentSpecValidator. We also import
+ # ModuleArgumentSpecValidator since that indicates that the 'classical' approach
+ # will no longer work.
+ from ansible.module_utils.common.arg_spec import ( # noqa: F401, pylint: disable=unused-import
+ ArgumentSpecValidator,
+ ModuleArgumentSpecValidator, # ModuleArgumentSpecValidator is not used
+ )
+ from ansible.module_utils.errors import UnsupportedError
+ HAS_ARGSPEC_VALIDATOR = True
+except ImportError:
+ # For ansible-base 2.10 and Ansible 2.9, we need to use the 'classical' approach
+ from ansible.module_utils.common.parameters import (
+ handle_aliases,
+ list_deprecations,
+ list_no_log_values,
+ )
+ HAS_ARGSPEC_VALIDATOR = False
+
+
+class _ModuleExitException(Exception):
+ def __init__(self, result):
+ super(_ModuleExitException, self).__init__()
+ self.result = result
+
+
+class AnsibleActionModule(object):
+ def __init__(self, action_plugin, argument_spec, bypass_checks=False,
+ mutually_exclusive=None, required_together=None,
+ required_one_of=None, supports_check_mode=False,
+ required_if=None, required_by=None):
+ # Internal data
+ self.__action_plugin = action_plugin
+ self.__warnings = []
+ self.__deprecations = []
+
+ # AnsibleModule data
+ self._name = self.__action_plugin._task.action
+ self.argument_spec = argument_spec
+ self.supports_check_mode = supports_check_mode
+ self.check_mode = self.__action_plugin._play_context.check_mode
+ self.bypass_checks = bypass_checks
+ self.no_log = self.__action_plugin._play_context.no_log
+
+ self.mutually_exclusive = mutually_exclusive
+ self.required_together = required_together
+ self.required_one_of = required_one_of
+ self.required_if = required_if
+ self.required_by = required_by
+ self._diff = self.__action_plugin._play_context.diff
+ self._verbosity = self.__action_plugin._display.verbosity
+
+ self.aliases = {}
+ self._legal_inputs = []
+ self._options_context = list()
+
+ self.params = copy.deepcopy(self.__action_plugin._task.args)
+ self.no_log_values = set()
+ if HAS_ARGSPEC_VALIDATOR:
+ self._validator = ArgumentSpecValidator(
+ self.argument_spec,
+ self.mutually_exclusive,
+ self.required_together,
+ self.required_one_of,
+ self.required_if,
+ self.required_by,
+ )
+ self._validation_result = self._validator.validate(self.params)
+ self.params.update(self._validation_result.validated_parameters)
+ self.no_log_values.update(self._validation_result._no_log_values)
+
+ try:
+ error = self._validation_result.errors[0]
+ except IndexError:
+ error = None
+
+ # We cannot use ModuleArgumentSpecValidator directly since it uses mechanisms for reporting
+ # warnings and deprecations that do not work in plugins. This is a copy of that code adjusted
+ # for our use-case:
+ for d in self._validation_result._deprecations:
+ # Before ansible-core 2.14.2, deprecations were always for aliases:
+ if 'name' in d:
+ self.deprecate(
+ "Alias '{name}' is deprecated. See the module docs for more information".format(name=d['name']),
+ version=d.get('version'), date=d.get('date'), collection_name=d.get('collection_name'))
+ # Since ansible-core 2.14.2, a message is present that can be directly printed:
+ if 'msg' in d:
+ self.deprecate(d['msg'], version=d.get('version'), date=d.get('date'), collection_name=d.get('collection_name'))
+
+ for w in self._validation_result._warnings:
+ self.warn('Both option {option} and its alias {alias} are set.'.format(option=w['option'], alias=w['alias']))
+
+ # Fail for validation errors, even in check mode
+ if error:
+ msg = self._validation_result.errors.msg
+ if isinstance(error, UnsupportedError):
+ msg = "Unsupported parameters for ({name}) {kind}: {msg}".format(name=self._name, kind='module', msg=msg)
+
+ self.fail_json(msg=msg)
+ else:
+ self._set_fallbacks()
+
+ # append to legal_inputs and then possibly check against them
+ try:
+ self.aliases = self._handle_aliases()
+ except (ValueError, TypeError) as e:
+ # Use exceptions here because it is not safe to call fail_json until no_log is processed
+ raise _ModuleExitException(dict(failed=True, msg="Module alias error: %s" % to_native(e)))
+
+ # Save parameter values that should never be logged
+ self._handle_no_log_values()
+
+ self._check_arguments()
+
+ # check exclusive early
+ if not bypass_checks:
+ self._check_mutually_exclusive(mutually_exclusive)
+
+ self._set_defaults(pre=True)
+
+ self._CHECK_ARGUMENT_TYPES_DISPATCHER = {
+ 'str': self._check_type_str,
+ 'list': check_type_list,
+ 'dict': check_type_dict,
+ 'bool': check_type_bool,
+ 'int': check_type_int,
+ 'float': check_type_float,
+ 'path': check_type_path,
+ 'raw': check_type_raw,
+ 'jsonarg': check_type_jsonarg,
+ 'json': check_type_jsonarg,
+ 'bytes': check_type_bytes,
+ 'bits': check_type_bits,
+ }
+ if not bypass_checks:
+ self._check_required_arguments()
+ self._check_argument_types()
+ self._check_argument_values()
+ self._check_required_together(required_together)
+ self._check_required_one_of(required_one_of)
+ self._check_required_if(required_if)
+ self._check_required_by(required_by)
+
+ self._set_defaults(pre=False)
+
+ # deal with options sub-spec
+ self._handle_options()
+
+ def _handle_aliases(self, spec=None, param=None, option_prefix=''):
+ if spec is None:
+ spec = self.argument_spec
+ if param is None:
+ param = self.params
+
+ # this uses exceptions as it happens before we can safely call fail_json
+ alias_warnings = []
+ alias_results, self._legal_inputs = handle_aliases(spec, param, alias_warnings=alias_warnings) # pylint: disable=used-before-assignment
+ for option, alias in alias_warnings:
+ self.warn('Both option %s and its alias %s are set.' % (option_prefix + option, option_prefix + alias))
+
+ deprecated_aliases = []
+ for i in spec.keys():
+ if 'deprecated_aliases' in spec[i].keys():
+ for alias in spec[i]['deprecated_aliases']:
+ deprecated_aliases.append(alias)
+
+ for deprecation in deprecated_aliases:
+ if deprecation['name'] in param.keys():
+ self.deprecate("Alias '%s' is deprecated. See the module docs for more information" % deprecation['name'],
+ version=deprecation.get('version'), date=deprecation.get('date'),
+ collection_name=deprecation.get('collection_name'))
+ return alias_results
+
+ def _handle_no_log_values(self, spec=None, param=None):
+ if spec is None:
+ spec = self.argument_spec
+ if param is None:
+ param = self.params
+
+ try:
+ self.no_log_values.update(list_no_log_values(spec, param)) # pylint: disable=used-before-assignment
+ except TypeError as te:
+ self.fail_json(msg="Failure when processing no_log parameters. Module invocation will be hidden. "
+ "%s" % to_native(te), invocation={'module_args': 'HIDDEN DUE TO FAILURE'})
+
+ for message in list_deprecations(spec, param): # pylint: disable=used-before-assignment
+ self.deprecate(message['msg'], version=message.get('version'), date=message.get('date'),
+ collection_name=message.get('collection_name'))
+
+ def _check_arguments(self, spec=None, param=None, legal_inputs=None):
+ self._syslog_facility = 'LOG_USER'
+ unsupported_parameters = set()
+ if spec is None:
+ spec = self.argument_spec
+ if param is None:
+ param = self.params
+ if legal_inputs is None:
+ legal_inputs = self._legal_inputs
+
+ for k in list(param.keys()):
+
+ if k not in legal_inputs:
+ unsupported_parameters.add(k)
+
+ for k in PASS_VARS:
+ # handle setting internal properties from internal ansible vars
+ param_key = '_ansible_%s' % k
+ if param_key in param:
+ if k in PASS_BOOLS:
+ setattr(self, PASS_VARS[k][0], self.boolean(param[param_key]))
+ else:
+ setattr(self, PASS_VARS[k][0], param[param_key])
+
+ # clean up internal top level params:
+ if param_key in self.params:
+ del self.params[param_key]
+ else:
+ # use defaults if not already set
+ if not hasattr(self, PASS_VARS[k][0]):
+ setattr(self, PASS_VARS[k][0], PASS_VARS[k][1])
+
+ if unsupported_parameters:
+ msg = "Unsupported parameters for (%s) module: %s" % (self._name, ', '.join(sorted(list(unsupported_parameters))))
+ if self._options_context:
+ msg += " found in %s." % " -> ".join(self._options_context)
+ supported_parameters = list()
+ for key in sorted(spec.keys()):
+ if 'aliases' in spec[key] and spec[key]['aliases']:
+ supported_parameters.append("%s (%s)" % (key, ', '.join(sorted(spec[key]['aliases']))))
+ else:
+ supported_parameters.append(key)
+ msg += " Supported parameters include: %s" % (', '.join(supported_parameters))
+ self.fail_json(msg=msg)
+ if self.check_mode and not self.supports_check_mode:
+ self.exit_json(skipped=True, msg="action module (%s) does not support check mode" % self._name)
+
+ def _count_terms(self, check, param=None):
+ if param is None:
+ param = self.params
+ return count_terms(check, param)
+
+ def _check_mutually_exclusive(self, spec, param=None):
+ if param is None:
+ param = self.params
+
+ try:
+ check_mutually_exclusive(spec, param)
+ except TypeError as e:
+ msg = to_native(e)
+ if self._options_context:
+ msg += " found in %s" % " -> ".join(self._options_context)
+ self.fail_json(msg=msg)
+
+ def _check_required_one_of(self, spec, param=None):
+ if spec is None:
+ return
+
+ if param is None:
+ param = self.params
+
+ try:
+ check_required_one_of(spec, param)
+ except TypeError as e:
+ msg = to_native(e)
+ if self._options_context:
+ msg += " found in %s" % " -> ".join(self._options_context)
+ self.fail_json(msg=msg)
+
+ def _check_required_together(self, spec, param=None):
+ if spec is None:
+ return
+ if param is None:
+ param = self.params
+
+ try:
+ check_required_together(spec, param)
+ except TypeError as e:
+ msg = to_native(e)
+ if self._options_context:
+ msg += " found in %s" % " -> ".join(self._options_context)
+ self.fail_json(msg=msg)
+
+ def _check_required_by(self, spec, param=None):
+ if spec is None:
+ return
+ if param is None:
+ param = self.params
+
+ try:
+ check_required_by(spec, param)
+ except TypeError as e:
+ self.fail_json(msg=to_native(e))
+
+ def _check_required_arguments(self, spec=None, param=None):
+ if spec is None:
+ spec = self.argument_spec
+ if param is None:
+ param = self.params
+
+ try:
+ check_required_arguments(spec, param)
+ except TypeError as e:
+ msg = to_native(e)
+ if self._options_context:
+ msg += " found in %s" % " -> ".join(self._options_context)
+ self.fail_json(msg=msg)
+
+ def _check_required_if(self, spec, param=None):
+ ''' ensure that parameters which conditionally required are present '''
+ if spec is None:
+ return
+ if param is None:
+ param = self.params
+
+ try:
+ check_required_if(spec, param)
+ except TypeError as e:
+ msg = to_native(e)
+ if self._options_context:
+ msg += " found in %s" % " -> ".join(self._options_context)
+ self.fail_json(msg=msg)
+
+ def _check_argument_values(self, spec=None, param=None):
+ ''' ensure all arguments have the requested values, and there are no stray arguments '''
+ if spec is None:
+ spec = self.argument_spec
+ if param is None:
+ param = self.params
+ for (k, v) in spec.items():
+ choices = v.get('choices', None)
+ if choices is None:
+ continue
+ if isinstance(choices, SEQUENCETYPE) and not isinstance(choices, (binary_type, text_type)):
+ if k in param:
+ # Allow one or more when type='list' param with choices
+ if isinstance(param[k], list):
+ diff_list = ", ".join([item for item in param[k] if item not in choices])
+ if diff_list:
+ choices_str = ", ".join([to_native(c) for c in choices])
+ msg = "value of %s must be one or more of: %s. Got no match for: %s" % (k, choices_str, diff_list)
+ if self._options_context:
+ msg += " found in %s" % " -> ".join(self._options_context)
+ self.fail_json(msg=msg)
+ elif param[k] not in choices:
+ # PyYaml converts certain strings to bools. If we can unambiguously convert back, do so before checking
+ # the value. If we cannot figure this out, module author is responsible.
+ lowered_choices = None
+ if param[k] == 'False':
+ lowered_choices = lenient_lowercase(choices)
+ overlap = BOOLEANS_FALSE.intersection(choices)
+ if len(overlap) == 1:
+ # Extract from a set
+ (param[k],) = overlap
+
+ if param[k] == 'True':
+ if lowered_choices is None:
+ lowered_choices = lenient_lowercase(choices)
+ overlap = BOOLEANS_TRUE.intersection(choices)
+ if len(overlap) == 1:
+ (param[k],) = overlap
+
+ if param[k] not in choices:
+ choices_str = ", ".join([to_native(c) for c in choices])
+ msg = "value of %s must be one of: %s, got: %s" % (k, choices_str, param[k])
+ if self._options_context:
+ msg += " found in %s" % " -> ".join(self._options_context)
+ self.fail_json(msg=msg)
+ else:
+ msg = "internal error: choices for argument %s are not iterable: %s" % (k, choices)
+ if self._options_context:
+ msg += " found in %s" % " -> ".join(self._options_context)
+ self.fail_json(msg=msg)
+
+ def safe_eval(self, value, locals=None, include_exceptions=False):
+ return safe_eval(value, locals, include_exceptions)
+
+ def _check_type_str(self, value, param=None, prefix=''):
+ opts = {
+ 'error': False,
+ 'warn': False,
+ 'ignore': True
+ }
+
+ # Ignore, warn, or error when converting to a string.
+ allow_conversion = opts.get(C.STRING_CONVERSION_ACTION, True)
+ try:
+ return check_type_str(value, allow_conversion)
+ except TypeError:
+ common_msg = 'quote the entire value to ensure it does not change.'
+ from_msg = '{0!r}'.format(value)
+ to_msg = '{0!r}'.format(to_text(value))
+
+ if param is not None:
+ if prefix:
+ param = '{0}{1}'.format(prefix, param)
+
+ from_msg = '{0}: {1!r}'.format(param, value)
+ to_msg = '{0}: {1!r}'.format(param, to_text(value))
+
+ if C.STRING_CONVERSION_ACTION == 'error':
+ msg = common_msg.capitalize()
+ raise TypeError(to_native(msg))
+ elif C.STRING_CONVERSION_ACTION == 'warn':
+ msg = ('The value "{0}" (type {1.__class__.__name__}) was converted to "{2}" (type string). '
+ 'If this does not look like what you expect, {3}').format(from_msg, value, to_msg, common_msg)
+ self.warn(to_native(msg))
+ return to_native(value, errors='surrogate_or_strict')
+
+ def _handle_options(self, argument_spec=None, params=None, prefix=''):
+ ''' deal with options to create sub spec '''
+ if argument_spec is None:
+ argument_spec = self.argument_spec
+ if params is None:
+ params = self.params
+
+ for (k, v) in argument_spec.items():
+ wanted = v.get('type', None)
+ if wanted == 'dict' or (wanted == 'list' and v.get('elements', '') == 'dict'):
+ spec = v.get('options', None)
+ if v.get('apply_defaults', False):
+ if spec is not None:
+ if params.get(k) is None:
+ params[k] = {}
+ else:
+ continue
+ elif spec is None or k not in params or params[k] is None:
+ continue
+
+ self._options_context.append(k)
+
+ if isinstance(params[k], dict):
+ elements = [params[k]]
+ else:
+ elements = params[k]
+
+ for idx, param in enumerate(elements):
+ if not isinstance(param, dict):
+ self.fail_json(msg="value of %s must be of type dict or list of dict" % k)
+
+ new_prefix = prefix + k
+ if wanted == 'list':
+ new_prefix += '[%d]' % idx
+ new_prefix += '.'
+
+ self._set_fallbacks(spec, param)
+ options_aliases = self._handle_aliases(spec, param, option_prefix=new_prefix)
+
+ options_legal_inputs = list(spec.keys()) + list(options_aliases.keys())
+
+ self._check_arguments(spec, param, options_legal_inputs)
+
+ # check exclusive early
+ if not self.bypass_checks:
+ self._check_mutually_exclusive(v.get('mutually_exclusive', None), param)
+
+ self._set_defaults(pre=True, spec=spec, param=param)
+
+ if not self.bypass_checks:
+ self._check_required_arguments(spec, param)
+ self._check_argument_types(spec, param, new_prefix)
+ self._check_argument_values(spec, param)
+
+ self._check_required_together(v.get('required_together', None), param)
+ self._check_required_one_of(v.get('required_one_of', None), param)
+ self._check_required_if(v.get('required_if', None), param)
+ self._check_required_by(v.get('required_by', None), param)
+
+ self._set_defaults(pre=False, spec=spec, param=param)
+
+ # handle multi level options (sub argspec)
+ self._handle_options(spec, param, new_prefix)
+ self._options_context.pop()
+
+ def _get_wanted_type(self, wanted, k):
+ if not callable(wanted):
+ if wanted is None:
+ # Mostly we want to default to str.
+ # For values set to None explicitly, return None instead as
+ # that allows a user to unset a parameter
+ wanted = 'str'
+ try:
+ type_checker = self._CHECK_ARGUMENT_TYPES_DISPATCHER[wanted]
+ except KeyError:
+ self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k))
+ else:
+ # set the type_checker to the callable, and reset wanted to the callable's name (or type if it does not have one, ala MagicMock)
+ type_checker = wanted
+ wanted = getattr(wanted, '__name__', to_native(type(wanted)))
+
+ return type_checker, wanted
+
+ def _handle_elements(self, wanted, param, values):
+ type_checker, wanted_name = self._get_wanted_type(wanted, param)
+ validated_params = []
+ # Get param name for strings so we can later display this value in a useful error message if needed
+ # Only pass 'kwargs' to our checkers and ignore custom callable checkers
+ kwargs = {}
+ if wanted_name == 'str' and isinstance(wanted, string_types):
+ if isinstance(param, string_types):
+ kwargs['param'] = param
+ elif isinstance(param, dict):
+ kwargs['param'] = list(param.keys())[0]
+ for value in values:
+ try:
+ validated_params.append(type_checker(value, **kwargs))
+ except (TypeError, ValueError) as e:
+ msg = "Elements value for option %s" % param
+ if self._options_context:
+ msg += " found in '%s'" % " -> ".join(self._options_context)
+ msg += " is of type %s and we were unable to convert to %s: %s" % (type(value), wanted_name, to_native(e))
+ self.fail_json(msg=msg)
+ return validated_params
+
+ def _check_argument_types(self, spec=None, param=None, prefix=''):
+ ''' ensure all arguments have the requested type '''
+
+ if spec is None:
+ spec = self.argument_spec
+ if param is None:
+ param = self.params
+
+ for (k, v) in spec.items():
+ wanted = v.get('type', None)
+ if k not in param:
+ continue
+
+ value = param[k]
+ if value is None:
+ continue
+
+ type_checker, wanted_name = self._get_wanted_type(wanted, k)
+ # Get param name for strings so we can later display this value in a useful error message if needed
+ # Only pass 'kwargs' to our checkers and ignore custom callable checkers
+ kwargs = {}
+ if wanted_name == 'str' and isinstance(type_checker, string_types):
+ kwargs['param'] = list(param.keys())[0]
+
+ # Get the name of the parent key if this is a nested option
+ if prefix:
+ kwargs['prefix'] = prefix
+
+ try:
+ param[k] = type_checker(value, **kwargs)
+ wanted_elements = v.get('elements', None)
+ if wanted_elements:
+ if wanted != 'list' or not isinstance(param[k], list):
+ msg = "Invalid type %s for option '%s'" % (wanted_name, param)
+ if self._options_context:
+ msg += " found in '%s'." % " -> ".join(self._options_context)
+ msg += ", elements value check is supported only with 'list' type"
+ self.fail_json(msg=msg)
+ param[k] = self._handle_elements(wanted_elements, k, param[k])
+
+ except (TypeError, ValueError) as e:
+ msg = "argument %s is of type %s" % (k, type(value))
+ if self._options_context:
+ msg += " found in '%s'." % " -> ".join(self._options_context)
+ msg += " and we were unable to convert to %s: %s" % (wanted_name, to_native(e))
+ self.fail_json(msg=msg)
+
+ def _set_defaults(self, pre=True, spec=None, param=None):
+ if spec is None:
+ spec = self.argument_spec
+ if param is None:
+ param = self.params
+ for (k, v) in spec.items():
+ default = v.get('default', None)
+ if pre is True:
+ # this prevents setting defaults on required items
+ if default is not None and k not in param:
+ param[k] = default
+ else:
+ # make sure things without a default still get set None
+ if k not in param:
+ param[k] = default
+
+ def _set_fallbacks(self, spec=None, param=None):
+ if spec is None:
+ spec = self.argument_spec
+ if param is None:
+ param = self.params
+
+ for (k, v) in spec.items():
+ fallback = v.get('fallback', (None,))
+ fallback_strategy = fallback[0]
+ fallback_args = []
+ fallback_kwargs = {}
+ if k not in param and fallback_strategy is not None:
+ for item in fallback[1:]:
+ if isinstance(item, dict):
+ fallback_kwargs = item
+ else:
+ fallback_args = item
+ try:
+ param[k] = fallback_strategy(*fallback_args, **fallback_kwargs)
+ except AnsibleFallbackNotFound:
+ continue
+
+ def warn(self, warning):
+ # Copied from ansible.module_utils.common.warnings:
+ if isinstance(warning, string_types):
+ self.__warnings.append(warning)
+ else:
+ raise TypeError("warn requires a string not a %s" % type(warning))
+
+ def deprecate(self, msg, version=None, date=None, collection_name=None):
+ if version is not None and date is not None:
+ raise AssertionError("implementation error -- version and date must not both be set")
+
+ # Copied from ansible.module_utils.common.warnings:
+ if isinstance(msg, string_types):
+ # For compatibility, we accept that neither version nor date is set,
+ # and treat that the same as if version would haven been set
+ if date is not None:
+ self.__deprecations.append({'msg': msg, 'date': date, 'collection_name': collection_name})
+ else:
+ self.__deprecations.append({'msg': msg, 'version': version, 'collection_name': collection_name})
+ else:
+ raise TypeError("deprecate requires a string not a %s" % type(msg))
+
+ def _return_formatted(self, kwargs):
+ if 'invocation' not in kwargs:
+ kwargs['invocation'] = {'module_args': self.params}
+
+ if 'warnings' in kwargs:
+ if isinstance(kwargs['warnings'], list):
+ for w in kwargs['warnings']:
+ self.warn(w)
+ else:
+ self.warn(kwargs['warnings'])
+
+ if self.__warnings:
+ kwargs['warnings'] = self.__warnings
+
+ if 'deprecations' in kwargs:
+ if isinstance(kwargs['deprecations'], list):
+ for d in kwargs['deprecations']:
+ if isinstance(d, SEQUENCETYPE) and len(d) == 2:
+ self.deprecate(d[0], version=d[1])
+ elif isinstance(d, Mapping):
+ self.deprecate(d['msg'], version=d.get('version'), date=d.get('date'),
+ collection_name=d.get('collection_name'))
+ else:
+ self.deprecate(d) # pylint: disable=ansible-deprecated-no-version
+ else:
+ self.deprecate(kwargs['deprecations']) # pylint: disable=ansible-deprecated-no-version
+
+ if self.__deprecations:
+ kwargs['deprecations'] = self.__deprecations
+
+ kwargs = remove_values(kwargs, self.no_log_values)
+ raise _ModuleExitException(kwargs)
+
+ def exit_json(self, **kwargs):
+ result = dict(kwargs)
+ if 'failed' not in result:
+ result['failed'] = False
+ self._return_formatted(result)
+
+ def fail_json(self, msg, **kwargs):
+ result = dict(kwargs)
+ result['failed'] = True
+ result['msg'] = msg
+ self._return_formatted(result)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class ActionModuleBase(ActionBase):
+ @abc.abstractmethod
+ def setup_module(self):
+ """Return pair (ArgumentSpec, kwargs)."""
+ pass
+
+ @abc.abstractmethod
+ def run_module(self, module):
+ """Run module code"""
+ module.fail_json(msg='Not implemented.')
+
+ def run(self, tmp=None, task_vars=None):
+ if task_vars is None:
+ task_vars = dict()
+
+ result = super(ActionModuleBase, self).run(tmp, task_vars)
+ del tmp # tmp no longer has any effect
+
+ try:
+ argument_spec, kwargs = self.setup_module()
+ module = argument_spec.create_ansible_module_helper(AnsibleActionModule, (self, ), **kwargs)
+ self.run_module(module)
+ raise AnsibleError('Internal error: action module did not call module.exit_json()')
+ except _ModuleExitException as mee:
+ result.update(mee.result)
+ return result
+ except Exception as dummy:
+ result['failed'] = True
+ result['msg'] = 'MODULE FAILURE'
+ result['exception'] = traceback.format_exc()
+ return result
diff --git a/ansible_collections/community/crypto/plugins/plugin_utils/filter_module.py b/ansible_collections/community/crypto/plugins/plugin_utils/filter_module.py
new file mode 100644
index 00000000..ce58317e
--- /dev/null
+++ b/ansible_collections/community/crypto/plugins/plugin_utils/filter_module.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2022 Felix Fontein <felix@fontein.de>
+# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+# NOTE: THIS IS ONLY FOR FILTER PLUGINS!
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+from ansible.errors import AnsibleFilterError
+
+
+class FilterModuleMock(object):
+ def __init__(self, params):
+ self.check_mode = True
+ self.params = params
+ self._diff = False
+
+ def fail_json(self, msg, **kwargs):
+ raise AnsibleFilterError(msg)