diff --git a/plugins/doc_fragments/acme.py b/plugins/doc_fragments/acme.py index 2f55cdda..e5ee3ec4 100644 --- a/plugins/doc_fragments/acme.py +++ b/plugins/doc_fragments/acme.py @@ -284,4 +284,34 @@ notes: or enabled with the O(select_crypto_backend) option. Note that using the C(openssl) binary will be slower." options: {} +''' + + CERTIFICATE = r''' +options: + csr: + description: + - "File containing the CSR for the new certificate." + - "Can be created with M(community.crypto.openssl_csr)." + - "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." + - "B(Note): the private key used to create the CSR B(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 O(csr) or O(csr_content) must be specified. + type: path + csr_content: + description: + - "Content of the CSR for the new certificate." + - "Can be created with M(community.crypto.openssl_csr_pipe)." + - "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." + - "B(Note): the private key used to create the CSR B(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 O(csr) or O(csr_content) must be specified. + type: str ''' diff --git a/plugins/module_utils/acme/acme.py b/plugins/module_utils/acme/acme.py index d89d7708..7f9b954c 100644 --- a/plugins/module_utils/acme/acme.py +++ b/plugins/module_utils/acme/acme.py @@ -420,45 +420,60 @@ class ACMEClient(object): return data -def get_default_argspec(with_account=True): +def get_default_argspec(): ''' Provides default argument spec for the options documented in the acme doc fragment. + + DEPRECATED: will be removed in community.crypto 3.0.0 ''' - argspec = dict( + return dict( 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), + 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'), ) - if with_account: - argspec.update(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'), - )) - return argspec -def create_default_argspec(with_account=True, require_account_key=True): +def create_default_argspec( + with_account=True, + require_account_key=True, + with_certificate=False, +): ''' Provides default argument spec for the options documented in the acme doc fragment. ''' result = ArgumentSpec( - get_default_argspec(with_account=with_account), + argument_spec=dict( + 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), + ), ) if with_account: + result.update_argspec( + 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'), + ) if require_account_key: - result.update( - required_one_of=[ - ['account_key_src', 'account_key_content'], - ], - ) + result.update(required_one_of=[['account_key_src', 'account_key_content']]) + result.update(mutually_exclusive=[['account_key_src', 'account_key_content']]) + if with_certificate: + result.update_argspec( + csr=dict(type='path'), + csr_content=dict(type='str'), + ) result.update( - mutually_exclusive=[ - ['account_key_src', 'account_key_content'], - ], + required_one_of=[['csr', 'csr_content']], + mutually_exclusive=[['csr', 'csr_content']], ) return result diff --git a/plugins/module_utils/acme/challenges.py b/plugins/module_utils/acme/challenges.py index 2b12c27c..116ca420 100644 --- a/plugins/module_utils/acme/challenges.py +++ b/plugins/module_utils/acme/challenges.py @@ -103,7 +103,7 @@ class Challenge(object): # 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) + record = '{0}.{1}'.format(resource, identifier[2:] if identifier.startswith('*.') else identifier) return { 'resource': resource, 'resource_value': value, diff --git a/plugins/module_utils/argspec.py b/plugins/module_utils/argspec.py index d6ba9c24..e583609d 100644 --- a/plugins/module_utils/argspec.py +++ b/plugins/module_utils/argspec.py @@ -47,7 +47,7 @@ class ArgumentSpec: return self def merge(self, other): - self.update_argspec(other.argument_spec) + self.update_argspec(**other.argument_spec) self.update( mutually_exclusive=other.mutually_exclusive, required_together=other.required_together, diff --git a/plugins/modules/acme_certificate.py b/plugins/modules/acme_certificate.py index 0272dd80..cba0cb96 100644 --- a/plugins/modules/acme_certificate.py +++ b/plugins/modules/acme_certificate.py @@ -82,6 +82,7 @@ seealso: extends_documentation_fragment: - community.crypto.acme.basic - community.crypto.acme.account + - community.crypto.acme.certificate - community.crypto.attributes - community.crypto.attributes.files - community.crypto.attributes.actiongroup_acme @@ -141,32 +142,8 @@ options: - 'tls-alpn-01' - 'no challenge' 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 O(csr) or O(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 O(csr) or O(csr_content) must be specified. - type: str version_added: 1.2.0 data: description: @@ -920,15 +897,14 @@ class ACMECertificateClient(object): def main(): - argument_spec = create_default_argspec() + argument_spec = create_default_argspec(with_certificate=True) + argument_spec.argument_spec['csr']['aliases'] = ['src'] argument_spec.update_argspec( 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', NO_CHALLENGE]), - 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']), @@ -947,13 +923,9 @@ def main(): include_renewal_cert_id=dict(type='str', choices=['never', 'when_ari_supported', 'always'], default='never'), ) argument_spec.update( - required_one_of=( + required_one_of=[ ['dest', 'fullchain_dest'], - ['csr', 'csr_content'], - ), - mutually_exclusive=( - ['csr', 'csr_content'], - ), + ], ) module = argument_spec.create_ansible_module(supports_check_mode=True) backend = create_backend(module, False)