mirror of
https://github.com/ansible-collections/community.crypto.git
synced 2026-05-06 13:22:58 +00:00
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:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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()")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user