mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
ACME: implement dns-account-01 challenge type (#996)
* Implement dns-account-01. * Bump draft versions. * dns-account-01 implementation changed in Pebble; only the one used by ansible-core 2.21/devel's ACME simulator matches the latest draft.
This commit is contained in:
@@ -149,9 +149,23 @@ class ACMECertificateClient:
|
||||
order.load_authorizations(client=self.client)
|
||||
return order
|
||||
|
||||
@staticmethod
|
||||
def _update_dns_data(
|
||||
data_dns: dict[str, list[str]],
|
||||
dns_challenge_type: str,
|
||||
challenge_data: dict[str, t.Any],
|
||||
) -> None:
|
||||
dns_challenge = challenge_data.get(dns_challenge_type)
|
||||
if dns_challenge:
|
||||
values = data_dns.get(dns_challenge["record"])
|
||||
if values is None:
|
||||
values = []
|
||||
data_dns[dns_challenge["record"]] = values
|
||||
values.append(dns_challenge["resource_value"])
|
||||
|
||||
def get_challenges_data(
|
||||
self, order: Order
|
||||
) -> tuple[list[dict[str, t.Any]], dict[str, list[str]]]:
|
||||
) -> tuple[list[dict[str, t.Any]], dict[str, list[str]], dict[str, list[str]]]:
|
||||
"""
|
||||
Get challenge details.
|
||||
|
||||
@@ -159,7 +173,9 @@ class ACMECertificateClient:
|
||||
"""
|
||||
data: list[dict[str, t.Any]] = []
|
||||
data_dns: dict[str, list[str]] = {}
|
||||
data_dns_account: dict[str, list[str]] = {}
|
||||
dns_challenge_type = "dns-01"
|
||||
dns_account_challenge_type = "dns-account-01"
|
||||
for authz in order.authorizations.values():
|
||||
# Skip valid authentications: their challenges are already valid
|
||||
# and do not need to be returned
|
||||
@@ -173,14 +189,11 @@ class ACMECertificateClient:
|
||||
"challenges": challenge_data,
|
||||
}
|
||||
)
|
||||
dns_challenge = challenge_data.get(dns_challenge_type)
|
||||
if dns_challenge:
|
||||
values = data_dns.get(dns_challenge["record"])
|
||||
if values is None:
|
||||
values = []
|
||||
data_dns[dns_challenge["record"]] = values
|
||||
values.append(dns_challenge["resource_value"])
|
||||
return data, data_dns
|
||||
self._update_dns_data(data_dns, dns_challenge_type, challenge_data)
|
||||
self._update_dns_data(
|
||||
data_dns_account, dns_account_challenge_type, challenge_data
|
||||
)
|
||||
return data, data_dns, data_dns_account
|
||||
|
||||
def check_that_authorizations_can_be_used(self, order: Order) -> None:
|
||||
bad_authzs = []
|
||||
|
||||
@@ -130,6 +130,26 @@ class Challenge:
|
||||
"record": record,
|
||||
}
|
||||
|
||||
if self.type == "dns-account-01":
|
||||
if identifier_type != "dns" or client.account_uri is None:
|
||||
return None
|
||||
# https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-account-label-02#section-3.2
|
||||
prefix = (
|
||||
base64.b32encode(
|
||||
hashlib.sha256(client.account_uri.encode("utf8")).digest()[:10]
|
||||
)
|
||||
.decode("ascii")
|
||||
.lower()
|
||||
)
|
||||
resource = f"_{prefix}._acme-challenge"
|
||||
value = nopad_b64(hashlib.sha256(to_bytes(key_authorization)).digest())
|
||||
record = f"{resource}.{identifier[2:] if identifier.startswith('*.') else 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":
|
||||
|
||||
@@ -12,17 +12,19 @@ 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/).
|
||||
The current implementation supports the V(http-01), V(dns-01) and V(tls-alpn-01) challenges.
|
||||
The current implementation supports the V(http-01), V(dns-01), V(dns-account-01) and V(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 O(data).
|
||||
- Between these two tasks you have to fulfill the required steps for the chosen challenge by whatever means necessary. For
|
||||
V(http-01) that means creating the necessary challenge file on the destination webserver. For V(dns-01) the necessary
|
||||
DNS record has to be created. For V(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.
|
||||
V(http-01) that means creating the necessary challenge file on the destination webserver. For V(dns-01) and V(dns-account-01)
|
||||
the necessary DNS records have to be created. For V(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).
|
||||
- The module support for IP identifiers according to L(RFC 8738,https://www.rfc-editor.org/rfc/rfc8738.html).
|
||||
- The module supports the V(dns-account-01) challenge type according to
|
||||
L(acme-dns-account-label draft 02, https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-account-label-02).
|
||||
notes:
|
||||
- At least one of O(dest) and O(fullchain_dest) must be specified.
|
||||
- This module includes basic account management functionality. If you want to have more control over your ACME account,
|
||||
@@ -115,13 +117,15 @@ options:
|
||||
- If set to V(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.
|
||||
- Support for V(dns-account-01) has been added in community.crypto 3.2.0.
|
||||
type: str
|
||||
default: 'http-01'
|
||||
default: http-01
|
||||
choices:
|
||||
- 'http-01'
|
||||
- 'dns-01'
|
||||
- 'tls-alpn-01'
|
||||
- 'no challenge'
|
||||
- http-01
|
||||
- dns-01
|
||||
- dns-account-01
|
||||
- tls-alpn-01
|
||||
- no challenge
|
||||
csr:
|
||||
aliases: ['src']
|
||||
csr_content:
|
||||
@@ -261,7 +265,7 @@ options:
|
||||
description:
|
||||
- Chose a specific profile for certificate selection. The available profiles depend on the CA.
|
||||
- See L(a blog post by Let's Encrypt, https://letsencrypt.org/2025/01/09/acme-profiles/) and
|
||||
L(draft-aaron-acme-profiles-00, https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/)
|
||||
L(draft-aaron-acme-profiles-01, https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/)
|
||||
for more information.
|
||||
type: str
|
||||
version_added: 2.24.0
|
||||
@@ -467,7 +471,7 @@ challenge_data:
|
||||
description:
|
||||
- Data for every challenge type.
|
||||
- The keys in this dictionary are the challenge types. C(challenge-type) is a placeholder used in the documentation.
|
||||
Possible keys are V(http-01), V(dns-01), and V(tls-alpn-01).
|
||||
Possible keys are V(http-01), V(dns-01), V(dns-account-01), and V(tls-alpn-01).
|
||||
- Note that the keys are not valid Jinja2 identifiers.
|
||||
returned: changed
|
||||
type: dict
|
||||
@@ -486,7 +490,7 @@ challenge_data:
|
||||
resource_value:
|
||||
description:
|
||||
- The value the resource has to produce for the validation.
|
||||
- For V(http-01) and V(dns-01) challenges, the value can be used as-is.
|
||||
- For V(http-01), V(dns-01), and V(dns-account-01) challenges, the value can be used as-is.
|
||||
- For V(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 P(ansible.builtin.b64decode#filter) Jinja filter to extract
|
||||
@@ -496,12 +500,12 @@ challenge_data:
|
||||
sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
|
||||
record:
|
||||
description: The full DNS record's name for the challenge.
|
||||
returned: changed and challenge is V(dns-01)
|
||||
returned: changed and challenge is V(dns-01) or V(dns-account-01)
|
||||
type: str
|
||||
sample: _acme-challenge.example.com
|
||||
challenge_data_dns:
|
||||
description:
|
||||
- List of TXT values per DNS record, in case challenge is V(dns-01).
|
||||
- List of TXT values per DNS record, in case challenge is V(dns-01) or V(dns-account-01).
|
||||
- Since Ansible 2.8.5, only challenges which are not yet valid are returned.
|
||||
returned: changed
|
||||
type: dict
|
||||
@@ -790,7 +794,10 @@ class ACMECertificateClient:
|
||||
raise ModuleFailException(
|
||||
f"Found no challenge of type '{self.challenge}' for identifier {type_identifier}!"
|
||||
)
|
||||
if self.challenge == "dns-01" and self.challenge in challenges:
|
||||
if (
|
||||
self.challenge in ("dns-01", "dns-account-01")
|
||||
and self.challenge in challenges
|
||||
):
|
||||
values = data_dns.get(challenges[self.challenge]["record"])
|
||||
if values is None:
|
||||
values = []
|
||||
@@ -974,7 +981,13 @@ def main() -> t.NoReturn:
|
||||
challenge={
|
||||
"type": "str",
|
||||
"default": "http-01",
|
||||
"choices": ["http-01", "dns-01", "tls-alpn-01", NO_CHALLENGE],
|
||||
"choices": [
|
||||
"http-01",
|
||||
"dns-01",
|
||||
"dns-account-01",
|
||||
"tls-alpn-01",
|
||||
NO_CHALLENGE,
|
||||
],
|
||||
},
|
||||
data={"type": "dict"},
|
||||
dest={"type": "path", "aliases": ["cert"]},
|
||||
|
||||
@@ -16,8 +16,8 @@ description:
|
||||
Authority such as L(Let's Encrypt,https://letsencrypt.org/).
|
||||
This module does not support ACME v1, the original version of the ACME protocol
|
||||
before standardization.
|
||||
- The current implementation supports the V(http-01), V(dns-01) and V(tls-alpn-01)
|
||||
challenges.
|
||||
- The current implementation supports the V(http-01), V(dns-01), V(dns-account-01),
|
||||
and V(tls-alpn-01) challenges.
|
||||
- This module needs to be used in conjunction with the
|
||||
M(community.crypto.acme_certificate_order_validate) and.
|
||||
M(community.crypto.acme_certificate_order_finalize) module.
|
||||
@@ -29,7 +29,7 @@ description:
|
||||
- Between the call of this module and M(community.crypto.acme_certificate_order_finalize),
|
||||
you have to fulfill the required steps for the chosen challenge by whatever means necessary.
|
||||
For V(http-01) that means creating the necessary challenge file on the destination webserver.
|
||||
For V(dns-01) the necessary dns record has to be created. For V(tls-alpn-01) the necessary
|
||||
For V(dns-01) and V(dns-account-01) the necessary DNS records have to be created. For V(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
|
||||
@@ -37,7 +37,9 @@ description:
|
||||
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 support for IP identifiers according to
|
||||
the L(RFC 8738,https://www.rfc-editor.org/rfc/rfc8738.html) ACME extension.
|
||||
L(RFC 8738,https://www.rfc-editor.org/rfc/rfc8738.html) ACME extension.
|
||||
- The module supports the V(dns-account-01) challenge type according to
|
||||
L(acme-dns-account-label draft 02, https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-account-label-02).
|
||||
seealso:
|
||||
- module: community.crypto.acme_certificate_order_validate
|
||||
description: Validate pending authorizations of an ACME order.
|
||||
@@ -122,7 +124,7 @@ options:
|
||||
description:
|
||||
- Chose a specific profile for certificate selection. The available profiles depend on the CA.
|
||||
- See L(a blog post by Let's Encrypt, https://letsencrypt.org/2025/01/09/acme-profiles/) and
|
||||
L(draft-aaron-acme-profiles-00, https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/)
|
||||
L(draft-aaron-acme-profiles-01, https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/)
|
||||
for more information.
|
||||
type: str
|
||||
order_creation_error_strategy:
|
||||
@@ -316,6 +318,31 @@ challenge_data:
|
||||
returned: success
|
||||
type: str
|
||||
sample: _acme-challenge.example.com
|
||||
dns-account-01:
|
||||
description:
|
||||
- Information for V(dns-account-01) authorization.
|
||||
- A DNS TXT record needs to be created with the record name RV(challenge_data[].challenges.dns-01.record)
|
||||
and value RV(challenge_data[].challenges.dns-01.resource_value).
|
||||
returned: if the identifier supports V(dns-account-01) authorization
|
||||
version_added: 3.2.0
|
||||
type: dict
|
||||
contains:
|
||||
resource:
|
||||
description:
|
||||
- Always ends with the string V(._acme-challenge).
|
||||
type: str
|
||||
sample: _ujmmovf2vn55tgye._acme-challenge
|
||||
resource_value:
|
||||
description:
|
||||
- The value the resource has to produce for the validation.
|
||||
returned: success
|
||||
type: str
|
||||
sample: IlirfxKKXA...17Dt3juxGJ-PCt92wr-oA
|
||||
record:
|
||||
description: The full DNS record's name for the challenge.
|
||||
returned: success
|
||||
type: str
|
||||
sample: _ujmmovf2vn55tgye._acme-challenge.example.com
|
||||
tls-alpn-01:
|
||||
description:
|
||||
- Information for V(tls-alpn-01) authorization.
|
||||
@@ -357,6 +384,13 @@ challenge_data_dns:
|
||||
- Only challenges which are not yet valid are returned.
|
||||
returned: success
|
||||
type: dict
|
||||
challenge_data_dns_account:
|
||||
description:
|
||||
- List of TXT values per DNS record for V(dns-account-01) challenges.
|
||||
- Only challenges which are not yet valid are returned.
|
||||
returned: success
|
||||
type: dict
|
||||
version_added: 3.2.0
|
||||
order_uri:
|
||||
description: ACME order URI.
|
||||
returned: success
|
||||
@@ -426,13 +460,14 @@ def main() -> t.NoReturn:
|
||||
finally:
|
||||
if module.params["deactivate_authzs"] and order and not done:
|
||||
client.deactivate_authzs(order)
|
||||
data, data_dns = client.get_challenges_data(order)
|
||||
data, data_dns, data_dns_account = client.get_challenges_data(order)
|
||||
module.exit_json(
|
||||
changed=True,
|
||||
order_uri=order.url,
|
||||
account_uri=client.client.account_uri,
|
||||
challenge_data=data,
|
||||
challenge_data_dns=data_dns,
|
||||
challenge_data_dns_account=data_dns_account,
|
||||
)
|
||||
except ModuleFailException as e:
|
||||
e.do_fail(module=module)
|
||||
|
||||
@@ -20,6 +20,8 @@ description:
|
||||
M(community.crypto.acme_certificate_order_create),
|
||||
M(community.crypto.acme_certificate_order_validate), and
|
||||
M(community.crypto.acme_certificate_order_finalize) modules.
|
||||
- The module supports the V(dns-account-01) challenge type according to
|
||||
L(acme-dns-account-label draft 02, https://datatracker.ietf.org/doc/html/draft-ietf-acme-dns-account-label-02).
|
||||
seealso:
|
||||
- module: community.crypto.acme_certificate_order_create
|
||||
description: Create an ACME order.
|
||||
@@ -256,11 +258,13 @@ authorizations_by_identifier:
|
||||
type:
|
||||
description:
|
||||
- The type of challenge encoded in the object.
|
||||
- Support for V(dns-account-01) has been added in community.crypto 3.2.0.
|
||||
type: str
|
||||
returned: always
|
||||
choices:
|
||||
- http-01
|
||||
- dns-01
|
||||
- dns-account-01
|
||||
- tls-alpn-01
|
||||
url:
|
||||
description:
|
||||
|
||||
@@ -65,11 +65,13 @@ options:
|
||||
- In case of authorization reuse, or in case of CAs which use External Account Binding
|
||||
and other means of validating certificate assurance, it might not be necessary
|
||||
to provide this option.
|
||||
- Support for V(dns-account-01) has been added in community.crypto 3.2.0.
|
||||
type: str
|
||||
choices:
|
||||
- 'http-01'
|
||||
- 'dns-01'
|
||||
- 'tls-alpn-01'
|
||||
- http-01
|
||||
- dns-01
|
||||
- dns-account-01
|
||||
- tls-alpn-01
|
||||
order_uri:
|
||||
description:
|
||||
- The order URI provided by RV(community.crypto.acme_certificate_order_create#module:order_uri).
|
||||
@@ -246,7 +248,10 @@ def main() -> t.NoReturn:
|
||||
argument_spec = create_default_argspec(with_certificate=False)
|
||||
argument_spec.update_argspec(
|
||||
order_uri={"type": "str", "required": True},
|
||||
challenge={"type": "str", "choices": ["http-01", "dns-01", "tls-alpn-01"]},
|
||||
challenge={
|
||||
"type": "str",
|
||||
"choices": ["http-01", "dns-01", "dns-account-01", "tls-alpn-01"],
|
||||
},
|
||||
deactivate_authzs={"type": "bool", "default": True},
|
||||
)
|
||||
module = argument_spec.create_ansible_module()
|
||||
|
||||
Reference in New Issue
Block a user