Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions plugins/doc_fragments/acme.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,4 +284,34 @@ class ModuleDocFragment(object):
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
'''
55 changes: 35 additions & 20 deletions plugins/module_utils/acme/acme.py
Original file line number Diff line number Diff line change
Expand Up @@ -420,45 +420,60 @@ def get_renewal_info(
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

Expand Down
2 changes: 1 addition & 1 deletion plugins/module_utils/acme/challenges.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def get_validation_data(self, client, identifier_type, identifier):
# 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,
Expand Down
2 changes: 1 addition & 1 deletion plugins/module_utils/argspec.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def update(self, mutually_exclusive=None, required_together=None, required_one_o
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,
Expand Down
38 changes: 5 additions & 33 deletions plugins/modules/acme_certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
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
Expand Down Expand Up @@ -141,32 +142,8 @@
- '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:
Expand Down Expand Up @@ -920,15 +897,14 @@ def deactivate_authzs(self):


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']),
Expand All @@ -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)
Expand Down
Loading