ACME modules: simplify code, refactor argspec handling code, move csr/csr_content to own docs fragment (#750)

* Fix bug in argspec module util.

* Move csr / csr_content to new docs fragment.

* Simplify code.

* Refactor ACME argspec creation. Add with_certificate argument for new CERTIFICATE docs fragment.
pull/751/head
Felix Fontein 2024-05-05 14:37:52 +02:00 committed by GitHub
parent f3c9cb7a8a
commit aa82575a78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 72 additions and 55 deletions

View File

@ -284,4 +284,34 @@ notes:
or enabled with the O(select_crypto_backend) option. Note that using or enabled with the O(select_crypto_backend) option. Note that using
the C(openssl) binary will be slower." the C(openssl) binary will be slower."
options: {} 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
''' '''

View File

@ -420,45 +420,60 @@ class ACMEClient(object):
return data 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. 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_directory=dict(type='str', required=True),
acme_version=dict(type='int', required=True, choices=[1, 2]), acme_version=dict(type='int', required=True, choices=[1, 2]),
validate_certs=dict(type='bool', default=True), validate_certs=dict(type='bool', default=True),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']), select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'openssl', 'cryptography']),
request_timeout=dict(type='int', default=10), request_timeout=dict(type='int', default=10),
)
if with_account:
argspec.update(dict(
account_key_src=dict(type='path', aliases=['account_key']), account_key_src=dict(type='path', aliases=['account_key']),
account_key_content=dict(type='str', no_log=True), account_key_content=dict(type='str', no_log=True),
account_key_passphrase=dict(type='str', no_log=True), account_key_passphrase=dict(type='str', no_log=True),
account_uri=dict(type='str'), 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. Provides default argument spec for the options documented in the acme doc fragment.
''' '''
result = ArgumentSpec( 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: 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: if require_account_key:
result.update( result.update(required_one_of=[['account_key_src', 'account_key_content']])
required_one_of=[ result.update(mutually_exclusive=[['account_key_src', 'account_key_content']])
['account_key_src', 'account_key_content'], if with_certificate:
], result.update_argspec(
csr=dict(type='path'),
csr_content=dict(type='str'),
) )
result.update( result.update(
mutually_exclusive=[ required_one_of=[['csr', 'csr_content']],
['account_key_src', 'account_key_content'], mutually_exclusive=[['csr', 'csr_content']],
],
) )
return result return result

View File

@ -103,7 +103,7 @@ class Challenge(object):
# https://tools.ietf.org/html/rfc8555#section-8.4 # https://tools.ietf.org/html/rfc8555#section-8.4
resource = '_acme-challenge' resource = '_acme-challenge'
value = nopad_b64(hashlib.sha256(to_bytes(key_authorization)).digest()) 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 { return {
'resource': resource, 'resource': resource,
'resource_value': value, 'resource_value': value,

View File

@ -47,7 +47,7 @@ class ArgumentSpec:
return self return self
def merge(self, other): def merge(self, other):
self.update_argspec(other.argument_spec) self.update_argspec(**other.argument_spec)
self.update( self.update(
mutually_exclusive=other.mutually_exclusive, mutually_exclusive=other.mutually_exclusive,
required_together=other.required_together, required_together=other.required_together,

View File

@ -82,6 +82,7 @@ seealso:
extends_documentation_fragment: extends_documentation_fragment:
- community.crypto.acme.basic - community.crypto.acme.basic
- community.crypto.acme.account - community.crypto.acme.account
- community.crypto.acme.certificate
- community.crypto.attributes - community.crypto.attributes
- community.crypto.attributes.files - community.crypto.attributes.files
- community.crypto.attributes.actiongroup_acme - community.crypto.attributes.actiongroup_acme
@ -141,32 +142,8 @@ options:
- 'tls-alpn-01' - 'tls-alpn-01'
- 'no challenge' - 'no challenge'
csr: 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'] aliases: ['src']
csr_content: 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 version_added: 1.2.0
data: data:
description: description:
@ -920,15 +897,14 @@ class ACMECertificateClient(object):
def main(): 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( argument_spec.update_argspec(
modify_account=dict(type='bool', default=True), modify_account=dict(type='bool', default=True),
account_email=dict(type='str'), account_email=dict(type='str'),
agreement=dict(type='str'), agreement=dict(type='str'),
terms_agreed=dict(type='bool', default=False), terms_agreed=dict(type='bool', default=False),
challenge=dict(type='str', default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01', NO_CHALLENGE]), 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'), data=dict(type='dict'),
dest=dict(type='path', aliases=['cert']), dest=dict(type='path', aliases=['cert']),
fullchain_dest=dict(type='path', aliases=['fullchain']), 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'), include_renewal_cert_id=dict(type='str', choices=['never', 'when_ari_supported', 'always'], default='never'),
) )
argument_spec.update( argument_spec.update(
required_one_of=( required_one_of=[
['dest', 'fullchain_dest'], ['dest', 'fullchain_dest'],
['csr', 'csr_content'], ],
),
mutually_exclusive=(
['csr', 'csr_content'],
),
) )
module = argument_spec.create_ansible_module(supports_check_mode=True) module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, False) backend = create_backend(module, False)