Bump version to 3.0.0-dev0, remove deprecated functionality and implement announced breaking changes (#873)

* Bump verison to 3.0.0-dev0.

* Change check mode behavior for *_pipe modules.

* Remove PyOpenSSL backend.

* Remove PyOpenSSL setup.

* Change default of asn1_base64.

* Remove deprecated common module utils.

* Remove get_default_argspec().

* Mark two methods as abstract.

* Remove ACME v1 support.

* Remove retrieve_acme_v1_certificate().

* Remove deprecated docs fragment.

* Change meaning of mode parameter.

* Mark no longer used option as 'to deprecate'.
This commit is contained in:
Felix Fontein
2025-04-29 08:12:44 +02:00
committed by GitHub
parent f73a1ce590
commit d368d1943d
41 changed files with 194 additions and 937 deletions

View File

@@ -53,61 +53,49 @@ class ACMEAccount(object):
"""
contact = contact or []
if self.client.version == 1:
new_reg = {"resource": "new-reg", "contact": contact}
if agreement:
new_reg["agreement"] = agreement
else:
new_reg["agreement"] = self.client.directory["meta"]["terms-of-service"]
if external_account_binding is not None:
raise ModuleFailException(
"External account binding is not supported for ACME v1"
)
url = self.client.directory["new-reg"]
else:
if (
external_account_binding is not None
or self.client.directory["meta"].get("externalAccountRequired")
) and allow_creation:
# Some ACME servers such as ZeroSSL do not like it when you try to register an existing account
# and provide external_account_binding credentials. Thus we first send a request with allow_creation=False
# to see whether the account already exists.
if (
external_account_binding is not None
or self.client.directory["meta"].get("externalAccountRequired")
) and allow_creation:
# Some ACME servers such as ZeroSSL do not like it when you try to register an existing account
# and provide external_account_binding credentials. Thus we first send a request with allow_creation=False
# to see whether the account already exists.
# Note that we pass contact here: ZeroSSL does not accept registration calls without contacts, even
# if onlyReturnExisting is set to true.
created, data = self._new_reg(contact=contact, allow_creation=False)
if data:
# An account already exists! Return data
return created, data
# An account does not yet exist. Try to create one next.
# Note that we pass contact here: ZeroSSL does not accept registration calls without contacts, even
# if onlyReturnExisting is set to true.
created, data = self._new_reg(contact=contact, allow_creation=False)
if data:
# An account already exists! Return data
return created, data
# An account does not yet exist. Try to create one next.
new_reg = {"contact": contact}
if not allow_creation:
# https://tools.ietf.org/html/rfc8555#section-7.3.1
new_reg["onlyReturnExisting"] = True
if terms_agreed:
new_reg["termsOfServiceAgreed"] = True
url = self.client.directory["newAccount"]
if external_account_binding is not None:
new_reg["externalAccountBinding"] = self.client.sign_request(
{
"alg": external_account_binding["alg"],
"kid": external_account_binding["kid"],
"url": url,
},
self.client.account_jwk,
self.client.backend.create_mac_key(
external_account_binding["alg"], external_account_binding["key"]
),
)
elif (
self.client.directory["meta"].get("externalAccountRequired")
and allow_creation
):
raise ModuleFailException(
"To create an account, an external account binding must be specified. "
"Use the acme_account module with the external_account_binding option."
)
new_reg = {"contact": contact}
if not allow_creation:
# https://tools.ietf.org/html/rfc8555#section-7.3.1
new_reg["onlyReturnExisting"] = True
if terms_agreed:
new_reg["termsOfServiceAgreed"] = True
url = self.client.directory["newAccount"]
if external_account_binding is not None:
new_reg["externalAccountBinding"] = self.client.sign_request(
{
"alg": external_account_binding["alg"],
"kid": external_account_binding["kid"],
"url": url,
},
self.client.account_jwk,
self.client.backend.create_mac_key(
external_account_binding["alg"], external_account_binding["key"]
),
)
elif (
self.client.directory["meta"].get("externalAccountRequired")
and allow_creation
):
raise ModuleFailException(
"To create an account, an external account binding must be specified. "
"Use the acme_account module with the external_account_binding option."
)
result, info = self.client.send_signed_request(
url, new_reg, fail_on_error=False
@@ -120,12 +108,12 @@ class ACMEAccount(object):
content=result,
)
if info["status"] in ([200, 201] if self.client.version == 1 else [201]):
if info["status"] == 201:
# Account did not exist
if "location" in info:
self.client.set_account_uri(info["location"])
return True, result
elif info["status"] == (409 if self.client.version == 1 else 200):
elif info["status"] == 200:
# Account did exist
if result.get("status") == "deactivated":
# A bug in Pebble (https://github.com/letsencrypt/pebble/issues/179) and
@@ -178,28 +166,21 @@ class ACMEAccount(object):
"""
if self.client.account_uri is None:
raise ModuleFailException("Account URI unknown")
if self.client.version == 1:
# try POST-as-GET first (draft-15 or newer)
data = None
result, info = self.client.send_signed_request(
self.client.account_uri, data, fail_on_error=False
)
# check whether that failed with a malformed request error
if (
info["status"] >= 400
and result.get("type") == "urn:ietf:params:acme:error:malformed"
):
# retry as a regular POST (with no changed data) for pre-draft-15 ACME servers
data = {}
data["resource"] = "reg"
result, info = self.client.send_signed_request(
self.client.account_uri, data, fail_on_error=False
)
else:
# try POST-as-GET first (draft-15 or newer)
data = None
result, info = self.client.send_signed_request(
self.client.account_uri, data, fail_on_error=False
)
# check whether that failed with a malformed request error
if (
info["status"] >= 400
and result.get("type") == "urn:ietf:params:acme:error:malformed"
):
# retry as a regular POST (with no changed data) for pre-draft-15 ACME servers
data = {}
result, info = self.client.send_signed_request(
self.client.account_uri, data, fail_on_error=False
)
if not isinstance(result, Mapping):
raise ACMEProtocolException(
self.client.module,
@@ -319,8 +300,6 @@ class ACMEAccount(object):
account_data = dict(account_data)
account_data.update(update_request)
else:
if self.client.version == 1:
update_request["resource"] = "reg"
account_data, info = self.client.send_signed_request(
self.client.account_uri, update_request
)

View File

@@ -142,12 +142,6 @@ class ACMEDirectory(object):
self.request_timeout = module.params["request_timeout"]
# Check whether self.version matches what we expect
if self.version == 1:
for key in ("new-reg", "new-authz", "new-cert"):
if key not in self.directory:
raise ModuleFailException(
"ACME directory does not seem to follow protocol ACME v1"
)
if self.version == 2:
for key in ("newNonce", "newAccount", "newOrder"):
if key not in self.directory:
@@ -168,7 +162,7 @@ class ACMEDirectory(object):
return self.directory.get(key, default_value)
def get_nonce(self, resource=None):
url = self.directory_root if self.version == 1 else self.directory["newNonce"]
url = self.directory["newNonce"]
if resource is not None:
url = resource
retry_count = 0
@@ -260,9 +254,8 @@ class ACMEClient(object):
requests.
"""
self.account_uri = uri
if self.version != 1:
self.account_jws_header.pop("jwk")
self.account_jws_header["kid"] = self.account_uri
self.account_jws_header.pop("jwk")
self.account_jws_header["kid"] = self.account_uri
def parse_key(self, key_file=None, key_content=None, passphrase=None):
"""
@@ -339,8 +332,7 @@ class ACMEClient(object):
while True:
protected = copy.deepcopy(jws_header)
protected["nonce"] = self.directory.get_nonce()
if self.version != 1:
protected["url"] = url
protected["url"] = url
self._log("URL", url)
self._log("protected", protected)
@@ -348,10 +340,6 @@ class ACMEClient(object):
data = self.sign_request(
protected, payload, key_data, encode_payload=encode_payload
)
if self.version == 1:
data["header"] = jws_header.copy()
for k, v in protected.items():
data["header"].pop(k, None)
self._log("signed request", data)
data = self.module.jsonify(data)
@@ -440,7 +428,7 @@ class ACMEClient(object):
Perform a GET-like request. Will try POST-as-GET for ACMEv2, with fallback
to GET if server replies with a status code of 405.
"""
if not get_only and self.version != 1:
if not get_only:
# Try POST-as-GET
content, info = self.send_signed_request(
uri, None, parse_json_result=False, fail_on_error=False
@@ -551,27 +539,6 @@ class ACMEClient(object):
return data
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
"""
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"),
)
def create_default_argspec(
with_account=True,
require_account_key=True,
@@ -583,7 +550,7 @@ def create_default_argspec(
result = ArgumentSpec(
argument_spec=dict(
acme_directory=dict(type="str", required=True),
acme_version=dict(type="int", required=True, choices=[1, 2]),
acme_version=dict(type="int", choices=[2], default=2),
validate_certs=dict(type="bool", default=True),
select_crypto_backend=dict(
type="str", default="auto", choices=["auto", "openssl", "cryptography"]
@@ -613,7 +580,7 @@ def create_default_argspec(
return result
def create_backend(module, needs_acme_v2):
def create_backend(module, needs_acme_v2=True):
if not HAS_IPADDRESS:
module.fail_json(
msg=missing_required_lib("ipaddress"), exception=IPADDRESS_IMPORT_ERROR
@@ -666,18 +633,6 @@ def create_backend(module, needs_acme_v2):
"development purposes, but *never* for production purposes."
)
if needs_acme_v2 and module.params["acme_version"] < 2:
module.fail_json(
msg="The {0} module requires the ACME v2 protocol!".format(module._name)
)
if module.params["acme_version"] == 1:
module.deprecate(
"The value 1 for 'acme_version' is deprecated. Please switch to ACME v2",
version="3.0.0",
collection_name="community.crypto",
)
# AnsibleModule() changes the locale, so change it back to C because we rely
# on datetime.datetime.strptime() when parsing certificate dates.
locale.setlocale(locale.LC_ALL, "C")

View File

@@ -150,6 +150,7 @@ class CryptoBackend(object):
def create_mac_key(self, alg, key):
"""Create a MAC key."""
@abc.abstractmethod
def get_ordered_csr_identifiers(self, csr_filename=None, csr_content=None):
"""
Return a list of requested identifiers (CN and SANs) for the CSR.
@@ -159,15 +160,6 @@ class CryptoBackend(object):
The list is deduplicated, and if a CNAME is present, it will be returned
as the first element in the result.
"""
self.module.deprecate(
"Every backend must override the get_ordered_csr_identifiers() method."
" The default implementation will be removed in 3.0.0 and this method will be marked as `abstractmethod` by then.",
version="3.0.0",
collection_name="community.crypto",
)
return sorted(
self.get_csr_identifiers(csr_filename=csr_filename, csr_content=csr_content)
)
@abc.abstractmethod
def get_csr_identifiers(self, csr_filename=None, csr_content=None):
@@ -193,11 +185,8 @@ class CryptoBackend(object):
Given a Criterium object, creates a ChainMatcher object.
"""
@abc.abstractmethod
def get_cert_information(self, cert_filename=None, cert_content=None):
"""
Return some information on a X.509 certificate as a CertificateInformation object.
"""
# Not implementing this method in a backend is DEPRECATED and will be
# disallowed in community.crypto 3.0.0. This method will be marked as
# @abstractmethod by then.
raise BackendException("This backend does not support get_cert_information()")

View File

@@ -19,7 +19,6 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
)
from ansible_collections.community.crypto.plugins.module_utils.acme.utils import (
der_to_pem,
nopad_b64,
process_links,
)
from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import (
@@ -116,33 +115,3 @@ class ChainMatcher(object):
"""
Check whether a certificate chain (CertificateChain instance) matches.
"""
def retrieve_acme_v1_certificate(client, csr_der):
"""
Create a new certificate based on the CSR (ACME v1 protocol).
Return the certificate object as dict
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
"""
new_cert = {
"resource": "new-cert",
"csr": nopad_b64(csr_der),
}
result, info = client.send_signed_request(
client.directory["new-cert"],
new_cert,
error_msg="Failed to receive certificate",
expected_status_codes=[200, 201],
)
cert = CertificateChain(info["location"])
cert.cert = der_to_pem(result)
def f(link, relation):
if relation == "up":
chain_result, chain_info = client.get_request(link, parse_json_result=False)
if chain_info["status"] in [200, 201]:
del cert.chain[:]
cert.chain.append(der_to_pem(chain_result))
process_links(info, f)
return cert

View File

@@ -79,16 +79,10 @@ class Challenge(object):
@classmethod
def from_json(cls, client, data, url=None):
return cls(data, url or (data["uri"] if client.version == 1 else data["url"]))
return cls(data, url or data["url"])
def call_validate(self, client):
challenge_response = {}
if client.version == 1:
token = re.sub(r"[^A-Za-z0-9_\-]", "_", self.token)
key_authorization = create_key_authorization(client, token)
challenge_response["resource"] = "challenge"
challenge_response["keyAuthorization"] = key_authorization
challenge_response["type"] = self.type
client.send_signed_request(
self.url,
challenge_response,
@@ -160,13 +154,7 @@ class Authorization(object):
]
else:
self.challenges = []
if client.version == 1 and "status" not in data:
# https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.1.2
# "status (required, string): ...
# If this field is missing, then the default value is "pending"."
self.status = "pending"
else:
self.status = data["status"]
self.status = data["status"]
self.identifier = data["identifier"]["value"]
self.identifier_type = data["identifier"]["type"]
if data.get("wildcard", False):
@@ -206,15 +194,11 @@ class Authorization(object):
"value": identifier,
},
}
if client.version == 1:
url = client.directory["new-authz"]
new_authz["resource"] = "new-authz"
else:
if "newAuthz" not in client.directory.directory:
raise ACMEProtocolException(
client.module, "ACME endpoint does not support pre-authorization"
)
url = client.directory["newAuthz"]
if "newAuthz" not in client.directory.directory:
raise ACMEProtocolException(
client.module, "ACME endpoint does not support pre-authorization"
)
url = client.directory["newAuthz"]
result, info = client.send_signed_request(
url,
@@ -338,8 +322,6 @@ class Authorization(object):
if not self.can_deactivate():
return
authz_deactivate = {"status": "deactivated"}
if client.version == 1:
authz_deactivate["resource"] = "authz"
result, info = client.send_signed_request(
self.url, authz_deactivate, fail_on_error=False
)
@@ -357,8 +339,6 @@ class Authorization(object):
"""
authz = cls(url)
authz_deactivate = {"status": "deactivated"}
if client.version == 1:
authz_deactivate["resource"] = "authz"
result, info = client.send_signed_request(
url, authz_deactivate, fail_on_error=True
)

View File

@@ -1,33 +0,0 @@
# -*- coding: utf-8 -*-
#
# Copyright (c) 2020, Felix Fontein <felix@fontein.de>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import absolute_import, division, print_function
__metaclass__ = type
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.crypto.plugins.module_utils.argspec import (
ArgumentSpec as _ArgumentSpec,
)
class ArgumentSpec(_ArgumentSpec):
def create_ansible_module_helper(self, clazz, args, **kwargs):
result = super(ArgumentSpec, self).create_ansible_module_helper(
clazz, args, **kwargs
)
result.deprecate(
"The crypto.module_backends.common module utils is deprecated and will be removed from community.crypto 3.0.0."
" Use the argspec module utils from community.crypto instead.",
version="3.0.0",
collection_name="community.crypto",
)
return result
__all__ = ("AnsibleModule", "ArgumentSpec")