From 17702d1a76779d08a6b2bf85ec3c21b5d6701cec Mon Sep 17 00:00:00 2001 From: Felix Fontein Date: Mon, 5 Jun 2023 20:54:07 +0200 Subject: [PATCH] acme_certificate: allow 'no challenge' (#615) * Allow 'no challenge'. * Fix undefined variable. --- changelogs/fragments/615-no-challenge.yml | 4 +++ plugins/modules/acme_certificate.py | 32 +++++++++++++++++++---- 2 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 changelogs/fragments/615-no-challenge.yml diff --git a/changelogs/fragments/615-no-challenge.yml b/changelogs/fragments/615-no-challenge.yml new file mode 100644 index 00000000..2e6ba6a0 --- /dev/null +++ b/changelogs/fragments/615-no-challenge.yml @@ -0,0 +1,4 @@ +minor_changes: + - "acme_certificate - allow to use no challenge by providing ``no challenge`` for the ``challenge`` option. + This is needed for ACME servers where validation is done without challenges + (https://github.com/ansible-collections/community.crypto/issues/613, https://github.com/ansible-collections/community.crypto/pull/615)." diff --git a/plugins/modules/acme_certificate.py b/plugins/modules/acme_certificate.py index 8ac7bd89..10f0cdef 100644 --- a/plugins/modules/acme_certificate.py +++ b/plugins/modules/acme_certificate.py @@ -124,10 +124,19 @@ options: type: bool default: true challenge: - description: The challenge to be performed. + description: + - The challenge to be performed. + - If set to C(no challenge), no challenge will be used. This is necessary for some private + CAs which use External Account Binding and other means of validating certificate assurance. + For example, an account could be allowed to issue certificates for C(foo.example.com) + without any further validation for a certain period of time. type: str default: 'http-01' - choices: [ 'http-01', 'dns-01', 'tls-alpn-01' ] + choices: + - 'http-01' + - 'dns-01' + - 'tls-alpn-01' + - 'no challenge' csr: description: - "File containing the CSR for the new certificate." @@ -578,6 +587,9 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import ) +NO_CHALLENGE = 'no challenge' + + class ACMECertificateClient(object): ''' ACME client class. Uses an ACME account object and a CSR to @@ -589,6 +601,9 @@ class ACMECertificateClient(object): self.module = module self.version = module.params['acme_version'] self.challenge = module.params['challenge'] + # We use None instead of a magic string for 'no challenge' + if self.challenge == NO_CHALLENGE: + self.challenge = None self.csr = module.params['csr'] self.csr_content = module.params['csr_content'] self.dest = module.params.get('dest') @@ -696,7 +711,7 @@ class ACMECertificateClient(object): 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]: + if first_step and self.challenge is not None 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 @@ -735,7 +750,14 @@ class ACMECertificateClient(object): 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) + if self.challenge is not None: + authz.call_validate(self.client, self.challenge) + # If there is no challenge, we must check whether the authz is valid + elif authz.status != 'valid': + authz.raise_error( + 'Status is not "valid", even though no challenge should be necessary', + module=self.client.module, + ) self.changed = True def download_alternate_chains(self, cert): @@ -832,7 +854,7 @@ def main(): 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']), + challenge=dict(type='str', default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01', NO_CHALLENGE]), csr=dict(type='path', aliases=['src']), csr_content=dict(type='str'), data=dict(type='dict'),