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

@@ -0,0 +1,14 @@
removed_features:
- "openssl_csr_pipe - the module now ignores check mode and will always behave as if check mode is not active (https://github.com/ansible-collections/community.crypto/pull/873)."
- "openssl_privatekey_pipe - the module now ignores check mode and will always behave as if check mode is not active (https://github.com/ansible-collections/community.crypto/pull/873)."
- "x509_certificate_pipe - the module now ignores check mode and will always behave as if check mode is not active (https://github.com/ansible-collections/community.crypto/pull/873)."
- "openssl_pkcs12 - support for the ``pyopenssl`` backend has been removed (https://github.com/ansible-collections/community.crypto/pull/873)."
- "crypto.module_backends.common module utils - this module utils has been removed. Use the ``argspec`` module utils instead (https://github.com/ansible-collections/community.crypto/pull/873)."
- "acme.acme module utils - the ``get_default_argspec()`` function has been removed. Use ``create_default_argspec()`` instead (https://github.com/ansible-collections/community.crypto/pull/873)."
- "acme.backends module utils - the methods ``get_ordered_csr_identifiers()`` and ``get_cert_information()`` of ``CryptoBackend`` now must be implemented (https://github.com/ansible-collections/community.crypto/pull/873)."
- "acme_* modules - support for ACME v1 has been removed (https://github.com/ansible-collections/community.crypto/pull/873)."
- "acme.documentation docs fragment - the ``documentation`` docs fragment has been removed. Use both the ``basic`` and ``account`` docs fragments in ``acme`` instead (https://github.com/ansible-collections/community.crypto/pull/873)."
breaking_changes:
- "get_certificate - the default for ``asn1_base64`` changed from ``false`` to ``true`` (https://github.com/ansible-collections/community.crypto/pull/873)."
- "acme.certificates module utils - the ``retrieve_acme_v1_certificate()`` helper function has been removed (https://github.com/ansible-collections/community.crypto/pull/873)."
- "x509_crl - the ``mode`` parameter no longer denotes the update mode, but the CRL file mode. Use ``crl_mode`` instead for the update mode (https://github.com/ansible-collections/community.crypto/pull/873)."

View File

@@ -5,7 +5,7 @@
namespace: community
name: crypto
version: 2.26.1
version: 3.0.0-dev0
readme: README.md
authors:
- Ansible (github.com/ansible)

View File

@@ -50,37 +50,15 @@ class PrivateKeyModule(object):
if self.module_backend.needs_regeneration():
# Regenerate
if not self.check_mode:
self.module_backend.generate_private_key()
privatekey_data = self.module_backend.get_private_key_data()
self.privatekey_bytes = privatekey_data
else:
self.module.deprecate(
"Check mode support for openssl_privatekey_pipe will change in community.crypto 3.0.0"
" to behave the same as without check mode. You can get that behavior right now"
" by adding `check_mode: false` to the openssl_privatekey_pipe task. If you think this"
" breaks your use-case of this module, please create an issue in the"
" community.crypto repository",
version="3.0.0",
collection_name="community.crypto",
)
self.module_backend.generate_private_key()
privatekey_data = self.module_backend.get_private_key_data()
self.privatekey_bytes = privatekey_data
self.changed = True
elif self.module_backend.needs_conversion():
# Convert
if not self.check_mode:
self.module_backend.convert_private_key()
privatekey_data = self.module_backend.get_private_key_data()
self.privatekey_bytes = privatekey_data
else:
self.module.deprecate(
"Check mode support for openssl_privatekey_pipe will change in community.crypto 3.0.0"
" to behave the same as without check mode. You can get that behavior right now"
" by adding `check_mode: false` to the openssl_privatekey_pipe task. If you think this"
" breaks your use-case of this module, please create an issue in the"
" community.crypto repository",
version="3.0.0",
collection_name="community.crypto",
)
self.module_backend.convert_private_key()
privatekey_data = self.module_backend.get_private_key_data()
self.privatekey_bytes = privatekey_data
self.changed = True
def dump(self):

View File

@@ -12,107 +12,6 @@ __metaclass__ = type
class ModuleDocFragment(object):
# Standard files documentation fragment
#
# NOTE: This document fragment is DEPRECATED and will be removed from community.crypto 3.0.0.
# Use both the BASIC and ACCOUNT fragments as a replacement.
DOCUMENTATION = r"""
notes:
- If a new enough version of the C(cryptography) library is available (see Requirements for details), it will be used instead
of the C(openssl) binary. This can be explicitly disabled or enabled with the O(select_crypto_backend) option. Note that
using the C(openssl) binary will be slower and less secure, as private key contents always have to be stored on disk (see
O(account_key_content)).
- Although the defaults are chosen so that the module can be used with the L(Let's Encrypt,https://letsencrypt.org/) CA,
the module can in principle be used with any CA providing an ACME endpoint, such as L(Buypass Go SSL,https://www.buypass.com/ssl/products/acme).
- So far, the ACME modules have only been tested by the developers against Let's Encrypt (staging and production), Buypass
(staging and production), ZeroSSL (production), and L(Pebble testing server,https://github.com/letsencrypt/Pebble). We
have got community feedback that they also work with Sectigo ACME Service for InCommon. If you experience problems with
another ACME server, please L(create an issue,https://github.com/ansible-collections/community.crypto/issues/new/choose)
to help us supporting it. Feedback that an ACME server not mentioned does work is also appreciated.
requirements:
- either openssl or L(cryptography,https://cryptography.io/) >= 1.5
- ipaddress
options:
account_key_src:
description:
- Path to a file containing the ACME account RSA or Elliptic Curve key.
- 'Private keys can be created with the M(community.crypto.openssl_privatekey) or M(community.crypto.openssl_privatekey_pipe)
modules. If the requisite (cryptography) is not available, keys can also be created directly with the C(openssl) command
line tool: RSA keys can be created with C(openssl genrsa ...). Elliptic curve keys can be created with C(openssl ecparam
-genkey ...). Any other tool creating private keys in PEM format can be used as well.'
- Mutually exclusive with O(account_key_content).
- Required if O(account_key_content) is not used.
type: path
aliases: [account_key]
account_key_content:
description:
- Content of the ACME account RSA or Elliptic Curve key.
- Mutually exclusive with O(account_key_src).
- Required if O(account_key_src) is not used.
- B(Warning:) the content will be written into a temporary file, which will be deleted by Ansible when the module completes.
Since this is an important private key — it can be used to change the account key, or to revoke your certificates
without knowing their private keys —, this might not be acceptable.
- In case C(cryptography) is used, the content is not written into a temporary file. It can still happen that it is
written to disk by Ansible in the process of moving the module with its argument to the node where it is executed.
type: str
account_key_passphrase:
description:
- Phassphrase to use to decode the account key.
- B(Note:) this is not supported by the C(openssl) backend, only by the C(cryptography) backend.
type: str
version_added: 1.6.0
account_uri:
description:
- If specified, assumes that the account URI is as given. If the account key does not match this account, or an account
with this URI does not exist, the module fails.
type: str
acme_version:
description:
- The ACME version of the endpoint.
- Must be V(1) for the classic Let's Encrypt and Buypass ACME endpoints, or V(2) for standardized ACME v2 endpoints.
- The value V(1) is deprecated since community.crypto 2.0.0 and will be removed from community.crypto 3.0.0.
required: true
type: int
choices: [1, 2]
acme_directory:
description:
- The ACME directory to use. This is the entry point URL to access the ACME CA server API.
- For safety reasons the default is set to the Let's Encrypt staging server (for the ACME v1 protocol). This will create
technically correct, but untrusted certificates.
- "For Let's Encrypt, all staging endpoints can be found here: U(https://letsencrypt.org/docs/staging-environment/).
For Buypass, all endpoints can be found here: U(https://community.buypass.com/t/63d4ay/buypass-go-ssl-endpoints)."
- For B(Let's Encrypt), the production directory URL for ACME v2 is U(https://acme-v02.api.letsencrypt.org/directory).
- For B(Buypass), the production directory URL for ACME v2 and v1 is U(https://api.buypass.com/acme/directory).
- For B(ZeroSSL), the production directory URL for ACME v2 is U(https://acme.zerossl.com/v2/DV90).
- For B(Sectigo), the production directory URL for ACME v2 is U(https://acme-qa.secure.trust-provider.com/v2/DV).
- The notes for this module contain a list of ACME services this module has been tested against.
required: true
type: str
validate_certs:
description:
- Whether calls to the ACME directory will validate TLS certificates.
- B(Warning:) Should B(only ever) be set to V(false) for testing purposes, for example when testing against a local
Pebble server.
type: bool
default: true
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is V(auto), which tries to use C(cryptography) if available, and falls back to C(openssl).
- If set to V(openssl), will try to use the C(openssl) binary.
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
type: str
default: auto
choices: [auto, cryptography, openssl]
request_timeout:
description:
- The time Ansible should wait for a response from the ACME API.
- This timeout is applied to all HTTP(S) requests (HEAD, GET, POST).
type: int
default: 10
version_added: 2.3.0
"""
# Basic documentation fragment without account data
BASIC = r"""
notes:
@@ -130,11 +29,12 @@ options:
acme_version:
description:
- The ACME version of the endpoint.
- Must be V(1) for the classic Let's Encrypt and Buypass ACME endpoints, or V(2) for standardized ACME v2 endpoints.
- The value V(1) is deprecated since community.crypto 2.0.0 and will be removed from community.crypto 3.0.0.
required: true
- Must be V(2) for standardized ACME v2 endpoints.
- The value V(1) is no longer supported since community.crypto 3.0.0.
type: int
choices: [1, 2]
default: 2
choices:
- 2
acme_directory:
description:
- The ACME directory to use. This is the entry point URL to access the ACME CA server API.

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")

View File

@@ -102,13 +102,13 @@ options:
description:
- URI to a terms of service document you agree to when using the ACME v1 service at O(acme_directory).
- Default is latest gathered from O(acme_directory) URL.
- This option will only be used when O(acme_version) is 1.
- This option has no longer any effect.
# TODO: deprecate!
type: str
terms_agreed:
description:
- Boolean indicating whether you agree to the terms of service document.
- ACME servers can require this to be true.
- This option will only be used when O(acme_version) is not 1.
type: bool
default: false
modify_account:
@@ -433,7 +433,6 @@ EXAMPLES = r"""
data: "{{ sample_com_challenge }}"
# We use Let's Encrypt's ACME v2 endpoint
acme_directory: https://acme-v02.api.letsencrypt.org/directory
acme_version: 2
# The following makes sure that if a chain with /CN=DST Root CA X3 in its issuer is provided
# as an alternative, it will be selected. These are the roots cross-signed by IdenTrust.
# As long as Let's Encrypt provides alternate chains with the cross-signed root(s) when
@@ -580,10 +579,8 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.acme import
from ansible_collections.community.crypto.plugins.module_utils.acme.certificates import (
CertificateChain,
Criterium,
retrieve_acme_v1_certificate,
)
from ansible_collections.community.crypto.plugins.module_utils.acme.challenges import (
Authorization,
combine_identifier,
normalize_combined_identifier,
split_identifier,
@@ -669,33 +666,21 @@ class ACMECertificateClient(object):
# Make sure account exists
modify_account = module.params["modify_account"]
if modify_account or self.version > 1:
contact = []
if module.params["account_email"]:
contact.append("mailto:" + module.params["account_email"])
created, account_data = self.account.setup_account(
contact,
agreement=module.params.get("agreement"),
terms_agreed=module.params.get("terms_agreed"),
allow_creation=modify_account,
)
if account_data is None:
raise ModuleFailException(
msg="Account does not exist or is deactivated."
)
updated = False
if not created and account_data and modify_account:
updated, account_data = self.account.update_account(
account_data, contact
)
self.changed = created or updated
else:
# This happens if modify_account is False and the ACME v1
# protocol is used. In this case, we do not call setup_account()
# to avoid accidental creation of an account. This is OK
# since for ACME v1, the account URI is not needed to send a
# signed ACME request.
pass
contact = []
if module.params["account_email"]:
contact.append("mailto:" + module.params["account_email"])
created, account_data = self.account.setup_account(
contact,
agreement=module.params.get("agreement"),
terms_agreed=module.params.get("terms_agreed"),
allow_creation=modify_account,
)
if account_data is None:
raise ModuleFailException(msg="Account does not exist or is deactivated.")
updated = False
if not created and account_data and modify_account:
updated, account_data = self.account.update_account(account_data, contact)
self.changed = created or updated
if self.csr is not None and not os.path.exists(self.csr):
raise ModuleFailException("CSR %s not found" % (self.csr))
@@ -712,13 +697,9 @@ class ACMECertificateClient(object):
"""
if self.data is None:
return True
if self.version == 1:
# As soon as self.data is a non-empty object, we are in the second stage.
return not self.data
else:
# We are in the second stage if data.order_uri is given (which has been
# stored in self.order_uri by the constructor).
return self.order_uri is None
# We are in the second stage if data.order_uri is given (which has been
# stored in self.order_uri by the constructor).
return self.order_uri is None
def _get_cert_info_or_none(self):
if self.module.params.get("dest"):
@@ -735,40 +716,30 @@ class ACMECertificateClient(object):
respectively start a new order for ACME v2.
"""
self.authorizations = {}
if self.version == 1:
for identifier_type, identifier in self.identifiers:
if identifier_type != "dns":
raise ModuleFailException("ACME v1 only supports DNS identifiers!")
for identifier_type, identifier in self.identifiers:
authz = Authorization.create(self.client, identifier_type, identifier)
self.authorizations[
normalize_combined_identifier(authz.combined_identifier)
] = authz
else:
replaces_cert_id = None
if self.include_renewal_cert_id == "always" or (
self.include_renewal_cert_id == "when_ari_supported"
and self.client.directory.has_renewal_info_endpoint()
):
cert_info = self._get_cert_info_or_none()
if cert_info is not None:
replaces_cert_id = compute_cert_id(
self.client.backend,
cert_info=cert_info,
none_if_required_information_is_missing=True,
)
self.order = Order.create_with_error_handling(
self.client,
self.identifiers,
error_strategy=self.order_creation_error_strategy,
error_max_retries=self.order_creation_max_retries,
replaces_cert_id=replaces_cert_id,
profile=self.profile,
message_callback=self.module.warn,
)
self.order_uri = self.order.url
self.order.load_authorizations(self.client)
self.authorizations.update(self.order.authorizations)
replaces_cert_id = None
if self.include_renewal_cert_id == "always" or (
self.include_renewal_cert_id == "when_ari_supported"
and self.client.directory.has_renewal_info_endpoint()
):
cert_info = self._get_cert_info_or_none()
if cert_info is not None:
replaces_cert_id = compute_cert_id(
self.client.backend,
cert_info=cert_info,
none_if_required_information_is_missing=True,
)
self.order = Order.create_with_error_handling(
self.client,
self.identifiers,
error_strategy=self.order_creation_error_strategy,
error_max_retries=self.order_creation_max_retries,
replaces_cert_id=replaces_cert_id,
profile=self.profile,
message_callback=self.module.warn,
)
self.order_uri = self.order.url
self.order.load_authorizations(self.client)
self.authorizations.update(self.order.authorizations)
self.changed = True
def get_challenges_data(self, first_step):
@@ -815,20 +786,11 @@ class ACMECertificateClient(object):
self.authorizations = {}
# Step 1: obtain challenge information
if self.version == 1:
# For ACME v1, we attempt to create new authzs. Existing ones
# will be returned instead.
for identifier_type, identifier in self.identifiers:
authz = Authorization.create(self.client, identifier_type, identifier)
self.authorizations[combine_identifier(identifier_type, identifier)] = (
authz
)
else:
# For ACME v2, we obtain the order object by fetching the
# order URI, and extract the information from there.
self.order = Order.from_url(self.client, self.order_uri)
self.order.load_authorizations(self.client)
self.authorizations.update(self.order.authorizations)
# For ACME v2, we obtain the order object by fetching the
# order URI, and extract the information from there.
self.order = Order.from_url(self.client, self.order_uri)
self.order.load_authorizations(self.client)
self.authorizations.update(self.order.authorizations)
# Step 2: validate pending challenges
authzs_to_wait_for = []
@@ -897,33 +859,25 @@ class ACMECertificateClient(object):
module=self.module,
)
if self.version == 1:
cert = retrieve_acme_v1_certificate(
self.client, pem_to_der(self.csr, self.csr_content)
)
else:
self.order.finalize(self.client, pem_to_der(self.csr, self.csr_content))
cert = CertificateChain.download(self.client, self.order.certificate_uri)
if (
self.module.params["retrieve_all_alternates"]
or self.select_chain_matcher
):
# Retrieve alternate chains
alternate_chains = self.download_alternate_chains(cert)
self.order.finalize(self.client, pem_to_der(self.csr, self.csr_content))
cert = CertificateChain.download(self.client, self.order.certificate_uri)
if self.module.params["retrieve_all_alternates"] or self.select_chain_matcher:
# Retrieve alternate chains
alternate_chains = self.download_alternate_chains(cert)
# Prepare return value for all alternate chains
if self.module.params["retrieve_all_alternates"]:
self.all_chains = [cert.to_json()]
for alt_chain in alternate_chains:
self.all_chains.append(alt_chain.to_json())
# Prepare return value for all alternate chains
if self.module.params["retrieve_all_alternates"]:
self.all_chains = [cert.to_json()]
for alt_chain in alternate_chains:
self.all_chains.append(alt_chain.to_json())
# Try to select alternate chain depending on criteria
if self.select_chain_matcher:
matching_chain = self.find_matching_chain([cert] + alternate_chains)
if matching_chain:
cert = matching_chain
else:
self.module.debug("Found no matching alternative chain")
# Try to select alternate chain depending on criteria
if self.select_chain_matcher:
matching_chain = self.find_matching_chain([cert] + alternate_chains)
if matching_chain:
cert = matching_chain
else:
self.module.debug("Found no matching alternative chain")
if cert.cert is not None:
pem_cert = cert.cert

View File

@@ -76,8 +76,6 @@ def main():
order_uri=dict(type="str", required=True),
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
if module.params["acme_version"] == 1:
module.fail_json("The module does not support acme_version=1")
backend = create_backend(module, False)

View File

@@ -215,7 +215,6 @@ EXAMPLES = r"""
- name: Create a challenge for sample.com using a account key file.
community.crypto.acme_certificate_order_create:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
@@ -238,7 +237,6 @@ EXAMPLES = r"""
- name: Let the challenge be validated
community.crypto.acme_certificate_order_validate:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
order_uri: "{{ sample_com_challenge.order_uri }}"
challenge: dns-01
@@ -246,7 +244,6 @@ EXAMPLES = r"""
- name: Retrieve the cert and intermediate certificate
community.crypto.acme_certificate_order_finalize:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
order_uri: "{{ sample_com_challenge.order_uri }}"
@@ -405,8 +402,6 @@ def main():
order_creation_max_retries=dict(type="int", default=3),
)
module = argument_spec.create_ansible_module()
if module.params["acme_version"] == 1:
module.fail_json("The module does not support acme_version=1")
backend = create_backend(module, False)

View File

@@ -227,7 +227,6 @@ EXAMPLES = r"""
- name: Create a challenge for sample.com using a account key file.
community.crypto.acme_certificate_order_create:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
@@ -250,7 +249,6 @@ EXAMPLES = r"""
- name: Let the challenge be validated
community.crypto.acme_certificate_order_validate:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
order_uri: "{{ sample_com_challenge.order_uri }}"
challenge: dns-01
@@ -258,7 +256,6 @@ EXAMPLES = r"""
- name: Retrieve the cert and intermediate certificate
community.crypto.acme_certificate_order_finalize:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
order_uri: "{{ sample_com_challenge.order_uri }}"
@@ -365,8 +362,6 @@ def main():
),
)
module = argument_spec.create_ansible_module()
if module.params["acme_version"] == 1:
module.fail_json("The module does not support acme_version=1")
backend = create_backend(module, False)

View File

@@ -380,8 +380,6 @@ def main():
order_uri=dict(type="str", required=True),
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
if module.params["acme_version"] == 1:
module.fail_json("The module does not support acme_version=1")
backend = create_backend(module, False)

View File

@@ -152,7 +152,6 @@ EXAMPLES = r"""
- name: Create a challenge for sample.com using a account key file.
community.crypto.acme_certificate_order_create:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
register: sample_com_challenge
@@ -175,7 +174,6 @@ EXAMPLES = r"""
- name: Let the challenge be validated
community.crypto.acme_certificate_order_validate:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
order_uri: "{{ sample_com_challenge.order_uri }}"
challenge: dns-01
@@ -183,7 +181,6 @@ EXAMPLES = r"""
- name: Retrieve the cert and intermediate certificate
community.crypto.acme_certificate_order_finalize:
acme_directory: https://acme-v01.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
csr: /etc/pki/cert/csr/sample.com.csr
order_uri: "{{ sample_com_challenge.order_uri }}"
@@ -257,8 +254,6 @@ def main():
deactivate_authzs=dict(type="bool", default=True),
)
module = argument_spec.create_ansible_module()
if module.params["acme_version"] == 1:
module.fail_json("The module does not support acme_version=1")
backend = create_backend(module, False)

View File

@@ -174,12 +174,7 @@ def main():
payload = {"certificate": certificate}
if module.params.get("revoke_reason") is not None:
payload["reason"] = module.params.get("revoke_reason")
# Determine endpoint
if module.params.get("acme_version") == 1:
endpoint = client.directory["revoke-cert"]
payload["resource"] = "revoke-cert"
else:
endpoint = client.directory["revokeCert"]
endpoint = client.directory["revokeCert"]
# Get hold of private key (if available) and make sure it comes from disk
private_key = module.params.get("private_key_src")
private_key_content = module.params.get("private_key_content")
@@ -227,12 +222,8 @@ def main():
already_revoked = True
else:
# Hack for Boulder errors
if module.params.get("acme_version") == 1:
error_type = "urn:acme:error:malformed"
else:
error_type = "urn:ietf:params:acme:error:malformed"
if (
result.get("type") == error_type
result.get("type") == "urn:ietf:params:acme:error:malformed"
and result.get("detail") == "Certificate already revoked"
):
# Fallback: boulder returns this in case the certificate was already revoked.

View File

@@ -27,7 +27,7 @@ notes:
- "Using the C(ansible) tool, M(community.crypto.acme_inspect) can be used to directly execute ACME requests without the
need of writing a playbook. For example, the following command retrieves the ACME account with ID 1 from Let's Encrypt
(assuming C(/path/to/key) is the correct private account key): C(ansible localhost -m acme_inspect -a \"account_key_src=/path/to/key
acme_directory=https://acme-v02.api.letsencrypt.org/directory acme_version=2 account_uri=https://acme-v02.api.letsencrypt.org/acme/acct/1
acme_directory=https://acme-v02.api.letsencrypt.org/directory account_uri=https://acme-v02.api.letsencrypt.org/acme/acct/1
method=get url=https://acme-v02.api.letsencrypt.org/acme/acct/1\")."
seealso:
- name: Automatic Certificate Management Environment (ACME)
@@ -83,14 +83,12 @@ EXAMPLES = r"""
- name: Get directory
community.crypto.acme_inspect:
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
acme_version: 2
method: directory-only
register: directory
- name: Create an account
community.crypto.acme_inspect:
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
url: "{{ directory.newAccount}}"
method: post
@@ -102,7 +100,6 @@ EXAMPLES = r"""
- name: Get account information
community.crypto.acme_inspect:
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
account_uri: "{{ account_creation.headers.location }}"
url: "{{ account_creation.headers.location }}"
@@ -111,7 +108,6 @@ EXAMPLES = r"""
- name: Update account contacts
community.crypto.acme_inspect:
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
account_uri: "{{ account_creation.headers.location }}"
url: "{{ account_creation.headers.location }}"
@@ -127,7 +123,6 @@ EXAMPLES = r"""
- name: Create certificate order
community.crypto.acme_certificate:
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
account_uri: "{{ account_creation.headers.location }}"
csr: /etc/pki/cert/csr/sample.com.csr
@@ -141,7 +136,6 @@ EXAMPLES = r"""
- name: Get order information
community.crypto.acme_inspect:
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
account_uri: "{{ account_creation.headers.location }}"
url: "{{ certificate_request.order_uri }}"
@@ -151,7 +145,6 @@ EXAMPLES = r"""
- name: Get first authz for order
community.crypto.acme_inspect:
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
account_uri: "{{ account_creation.headers.location }}"
url: "{{ order.output_json.authorizations[0] }}"
@@ -161,7 +154,6 @@ EXAMPLES = r"""
- name: Get HTTP-01 challenge for authz
community.crypto.acme_inspect:
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
account_uri: "{{ account_creation.headers.location }}"
url: "{{ authz.output_json.challenges | selectattr('type', 'equalto', 'http-01') }}"
@@ -171,7 +163,6 @@ EXAMPLES = r"""
- name: Activate HTTP-01 challenge manually
community.crypto.acme_inspect:
acme_directory: https://acme-staging-v02.api.letsencrypt.org/directory
acme_version: 2
account_key_src: /etc/pki/cert/private/account.key
account_uri: "{{ account_creation.headers.location }}"
url: "{{ http01challenge.url }}"

View File

@@ -100,8 +100,9 @@ options:
- Whether to encode the ASN.1 values in the RV(extensions) return value with Base64 or not.
- The documentation claimed for a long time that the values are Base64 encoded, but they never were. For compatibility
this option is set to V(false).
- The default value V(false) is B(deprecated) and will change to V(true) in community.crypto 3.0.0.
- The default value was changed from V(false) to V(true) incommunity.crypto 3.0.0.
type: bool
default: true
version_added: 2.12.0
tls_ctx_options:
description:
@@ -351,7 +352,7 @@ def main():
),
starttls=dict(type="str", choices=["mysql"]),
ciphers=dict(type="list", elements="str"),
asn1_base64=dict(type="bool"),
asn1_base64=dict(type="bool", default=True),
tls_ctx_options=dict(type="list", elements="raw"),
get_certificate_chain=dict(type="bool", default=False),
),
@@ -370,16 +371,6 @@ def main():
tls_ctx_options = module.params["tls_ctx_options"]
get_certificate_chain = module.params["get_certificate_chain"]
if asn1_base64 is None:
module.deprecate(
"The default value `false` for asn1_base64 is deprecated and will change to `true` in "
"community.crypto 3.0.0. If you need this value, it is best to set the value explicitly "
"and adjust your roles/playbooks to use `asn1_base64=true` as soon as possible",
version="3.0.0",
collection_name="community.crypto",
)
asn1_base64 = False
if get_certificate_chain and sys.version_info < (3, 10):
module.fail_json(
msg="get_certificate_chain=true can only be used with Python 3.10 (Python 3.13+ officially supports this). "

View File

@@ -29,10 +29,7 @@ attributes:
check_mode:
support: full
details:
- Currently in check mode, private keys will not be (re-)generated, only the changed status is set. This will change
in community.crypto 3.0.0.
- From community.crypto 3.0.0 on, the module will ignore check mode and always behave as if check mode is not active.
If you think this breaks your use-case of this module, please create an issue in the community.crypto repository.
- Since community.crypto 3.0.0, the module ignores check mode and always behaves as if check mode is not active.
options:
content:
description:
@@ -157,18 +154,7 @@ class CertificateSigningRequestModule(object):
def generate(self, module):
"""Generate the certificate signing request."""
if self.module_backend.needs_regeneration():
if not self.check_mode:
self.module_backend.generate_csr()
else:
self.module.deprecate(
"Check mode support for openssl_csr_pipe will change in community.crypto 3.0.0"
" to behave the same as without check mode. You can get that behavior right now"
" by adding `check_mode: false` to the openssl_csr_pipe task. If you think this"
" breaks your use-case of this module, please create an issue in the"
" community.crypto repository",
version="3.0.0",
collection_name="community.crypto",
)
self.module_backend.generate_csr()
self.changed = True
def dump(self):

View File

@@ -18,11 +18,9 @@ author:
short_description: Generate OpenSSL PKCS#12 archive
description:
- This module allows one to (re-)generate PKCS#12.
- The module can use the cryptography Python library, or the pyOpenSSL Python library. By default, it tries to detect which
one is available, assuming none of the O(iter_size) and O(maciter_size) options are used. This can be overridden with
the O(select_crypto_backend) option.
- The module uses the cryptography Python library.
requirements:
- PyOpenSSL >= 0.15, < 23.3.0 or cryptography >= 3.0
- cryptography >= 3.0
extends_documentation_fragment:
- ansible.builtin.files
- community.crypto.attributes
@@ -95,15 +93,16 @@ options:
description:
- Number of times to repeat the encryption step.
- This is B(not considered during idempotency checks).
- This is only used by the C(pyopenssl) backend, or when O(encryption_level=compatibility2022).
- When using it, the default is V(2048) for C(pyopenssl) and V(50000) for C(cryptography).
- This is only used when O(encryption_level=compatibility2022).
- When using it, the default is V(50000).
type: int
maciter_size:
description:
- Number of times to repeat the MAC step.
- This is B(not considered during idempotency checks).
- This is only used by the C(pyopenssl) backend. When using it, the default is V(1).
- This value is B(not used).
type: int
# TODO: deprecate!
encryption_level:
description:
- Determines the encryption level used.
@@ -170,15 +169,12 @@ options:
select_crypto_backend:
description:
- Determines which crypto backend to use.
- The default choice is V(auto), which tries to use C(cryptography) if available, and falls back to C(pyopenssl). If
O(iter_size) is used together with O(encryption_level) is not V(compatibility2022), or if O(maciter_size) is used,
V(auto) will always result in C(pyopenssl) to be chosen for backwards compatibility.
- If set to V(pyopenssl), will try to use the L(pyOpenSSL,https://pypi.org/project/pyOpenSSL/) library.
- The default choice is V(auto), which tries to use C(cryptography) if available.
- If set to V(cryptography), will try to use the L(cryptography,https://cryptography.io/) library.
- B(Note) that the V(pyopenssl) backend is deprecated and will be removed from community.crypto 3.0.0.
- The value V(pyopenssl) has been removed for community.crypto 3.0.0.
type: str
default: auto
choices: [auto, cryptography, pyopenssl]
choices: [auto, cryptography]
version_added: 1.7.0
seealso:
- module: community.crypto.x509_certificate
@@ -315,23 +311,6 @@ from ansible_collections.community.crypto.plugins.module_utils.version import (
MINIMAL_CRYPTOGRAPHY_VERSION = "3.0"
MINIMAL_PYOPENSSL_VERSION = "0.15"
MAXIMAL_PYOPENSSL_VERSION = "23.3.0"
PYOPENSSL_IMP_ERR = None
try:
import OpenSSL
from OpenSSL import crypto
from OpenSSL.crypto import (
load_pkcs12 as _load_pkcs12, # this got removed in pyOpenSSL 23.3.0
)
PYOPENSSL_VERSION = LooseVersion(OpenSSL.__version__)
except (ImportError, AttributeError):
PYOPENSSL_IMP_ERR = traceback.format_exc()
PYOPENSSL_FOUND = False
else:
PYOPENSSL_FOUND = True
CRYPTOGRAPHY_IMP_ERR = None
try:
@@ -628,88 +607,6 @@ class Pkcs(OpenSSLObject):
self.pkcs12_bytes = content
class PkcsPyOpenSSL(Pkcs):
def __init__(self, module):
super(PkcsPyOpenSSL, self).__init__(module, "pyopenssl")
if self.encryption_level != "auto":
module.fail_json(
msg="The PyOpenSSL backend only supports encryption_level = auto"
)
def generate_bytes(self, module):
"""Generate PKCS#12 file archive."""
self.pkcs12 = crypto.PKCS12()
if self.other_certificates:
self.pkcs12.set_ca_certificates(self.other_certificates)
if self.certificate_content:
self.pkcs12.set_certificate(
load_certificate(
None, content=self.certificate_content, backend=self.backend
)
)
if self.friendly_name:
self.pkcs12.set_friendlyname(to_bytes(self.friendly_name))
if self.privatekey_content:
try:
self.pkcs12.set_privatekey(
load_privatekey(
None,
content=self.privatekey_content,
passphrase=self.privatekey_passphrase,
backend=self.backend,
)
)
except OpenSSLBadPassphraseError as exc:
raise PkcsError(exc)
return self.pkcs12.export(self.passphrase, self.iter_size, self.maciter_size)
def parse_bytes(self, pkcs12_content):
try:
p12 = crypto.load_pkcs12(pkcs12_content, self.passphrase)
pkey = p12.get_privatekey()
if pkey is not None:
pkey = crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)
crt = p12.get_certificate()
if crt is not None:
crt = crypto.dump_certificate(crypto.FILETYPE_PEM, crt)
other_certs = []
if p12.get_ca_certificates() is not None:
other_certs = [
crypto.dump_certificate(crypto.FILETYPE_PEM, other_cert)
for other_cert in p12.get_ca_certificates()
]
friendly_name = p12.get_friendlyname()
return (pkey, crt, other_certs, friendly_name)
except crypto.Error as exc:
raise PkcsError(exc)
def _dump_privatekey(self, pkcs12):
pk = pkcs12.get_privatekey()
return crypto.dump_privatekey(crypto.FILETYPE_PEM, pk) if pk else None
def _dump_certificate(self, pkcs12):
cert = pkcs12.get_certificate()
return crypto.dump_certificate(crypto.FILETYPE_PEM, cert) if cert else None
def _dump_other_certificates(self, pkcs12):
if pkcs12.get_ca_certificates() is None:
return []
return [
crypto.dump_certificate(crypto.FILETYPE_PEM, other_cert)
for other_cert in pkcs12.get_ca_certificates()
]
def _get_friendly_name(self, pkcs12):
return pkcs12.get_friendlyname()
class PkcsCryptography(Pkcs):
def __init__(self, module):
super(PkcsCryptography, self).__init__(
@@ -839,52 +736,20 @@ def select_backend(module, backend):
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
)
can_use_pyopenssl = (
PYOPENSSL_FOUND
and PYOPENSSL_VERSION >= LooseVersion(MINIMAL_PYOPENSSL_VERSION)
and PYOPENSSL_VERSION < LooseVersion(MAXIMAL_PYOPENSSL_VERSION)
)
# If no restrictions are provided, first try cryptography, then pyOpenSSL
if (
module.params["iter_size"] is not None
and module.params["encryption_level"] != "compatibility2022"
) or module.params["maciter_size"] is not None:
# If iter_size (for encryption_level != compatibility2022) or maciter_size is specified, use pyOpenSSL backend
backend = "pyopenssl"
elif can_use_cryptography:
if can_use_cryptography:
backend = "cryptography"
elif can_use_pyopenssl:
backend = "pyopenssl"
# Success?
if backend == "auto":
module.fail_json(
msg=(
"Cannot detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1}, < {2})"
"Cannot detect the required Python library cryptography (>= {0})"
).format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION,
MAXIMAL_PYOPENSSL_VERSION,
)
)
if backend == "pyopenssl":
if not PYOPENSSL_FOUND:
msg = missing_required_lib(
"pyOpenSSL >= {0}, < {1}".format(
MINIMAL_PYOPENSSL_VERSION, MAXIMAL_PYOPENSSL_VERSION
)
)
module.fail_json(msg=msg, exception=PYOPENSSL_IMP_ERR)
module.deprecate(
"The module is using the PyOpenSSL backend. This backend has been deprecated",
version="3.0.0",
collection_name="community.crypto",
)
return backend, PkcsPyOpenSSL(module)
elif backend == "cryptography":
if backend == "cryptography":
if not CRYPTOGRAPHY_FOUND:
module.fail_json(
msg=missing_required_lib(
@@ -924,7 +789,7 @@ def main():
backup=dict(type="bool", default=False),
return_content=dict(type="bool", default=False),
select_crypto_backend=dict(
type="str", default="auto", choices=["auto", "cryptography", "pyopenssl"]
type="str", default="auto", choices=["auto", "cryptography"]
),
)

View File

@@ -39,10 +39,7 @@ attributes:
check_mode:
support: full
details:
- Currently in check mode, private keys will not be (re-)generated, only the changed status is set. This will change
in community.crypto 3.0.0.
- From community.crypto 3.0.0 on, the module will ignore check mode and always behave as if check mode is not active.
If you think this breaks your use-case of this module, please create an issue in the community.crypto repository.
- Since community.crypto 3.0.0, the module ignores check mode and always behaves as if check mode is not active.
options:
content:
description:

View File

@@ -33,10 +33,7 @@ attributes:
check_mode:
support: full
details:
- Currently in check mode, private keys will not be (re-)generated, only the changed status is set. This will change
in community.crypto 3.0.0.
- From community.crypto 3.0.0 on, the module will ignore check mode and always behave as if check mode is not active.
If you think this breaks your use-case of this module, please create an issue in the community.crypto repository.
- Since community.crypto 3.0.0 the module ignores check mode and always behaves as if check mode is not active.
options:
provider:
description:
@@ -162,18 +159,7 @@ class GenericCertificate(object):
def generate(self, module):
if self.module_backend.needs_regeneration():
if not self.check_mode:
self.module_backend.generate_certificate()
else:
self.module.deprecate(
"Check mode support for x509_certificate_pipe will change in community.crypto 3.0.0"
" to behave the same as without check mode. You can get that behavior right now"
" by adding `check_mode: false` to the x509_certificate_pipe task. If you think this"
" breaks your use-case of this module, please create an issue in the"
" community.crypto repository",
version="3.0.0",
collection_name="community.crypto",
)
self.module_backend.generate_certificate()
self.changed = True
def dump(self, check_mode=False):

View File

@@ -59,17 +59,9 @@ options:
- This parameter was called O(mode) before community.crypto 2.13.0. It has been renamed to avoid a collision with the
common O(mode) parameter for setting the CRL file's access mode.
type: str
# default: generate
default: generate
choices: [generate, update]
version_added: 2.13.0
mode:
description:
- This parameter has been renamed to O(crl_mode). The old name O(mode) is now deprecated and will be removed in community.crypto
3.0.0. Replace usage of this parameter with O(crl_mode).
- Note that from community.crypto 3.0.0 on, O(mode) will be used for the CRL file's mode.
type: str
# default: generate
choices: [generate, update]
force:
description:
@@ -968,16 +960,9 @@ def main():
state=dict(type="str", default="present", choices=["present", "absent"]),
crl_mode=dict(
type="str",
# default='generate',
default="generate",
choices=["generate", "update"],
),
mode=dict(
type="str",
# default='generate',
choices=["generate", "update"],
removed_in_version="3.0.0",
removed_from_collection="community.crypto",
),
force=dict(type="bool", default=False),
backup=dict(type="bool", default=False),
path=dict(type="path", required=True),
@@ -1044,16 +1029,6 @@ def main():
add_file_common_args=True,
)
if module.params["mode"]:
if module.params["crl_mode"]:
module.fail_json(
"You cannot use both `mode` and `crl_mode`. Use `crl_mode`."
)
module.params["crl_mode"] = module.params["mode"]
# TODO: in 3.0.0, once the option `mode` has been removed, remove this:
module.params.pop("mode", None)
# From then on, `mode` will be the file mode of the CRL file
if not CRYPTOGRAPHY_FOUND:
module.fail_json(
msg=missing_required_lib(

View File

@@ -5,6 +5,5 @@
dependencies:
- setup_acme
- setup_pyopenssl # needed for Ubuntu 16.04
- setup_remote_tmp_dir
- prepare_jinja2_compat

View File

@@ -5,5 +5,4 @@
dependencies:
- setup_openssl
- setup_pyopenssl
- setup_remote_tmp_dir

View File

@@ -62,17 +62,6 @@
path: '{{ remote_tmp_dir }}/ansible.p12'
state: absent
- block:
- name: Running tests with pyOpenSSL backend
include_tasks: impl.yml
vars:
select_crypto_backend: pyopenssl
when: >-
(pyopenssl_version.stdout | default('0.0')) is version('0.15', '>=')
and
(pyopenssl_version.stdout | default('0.0')) is version('23.3.0', '<')
- block:
- name: Running tests with cryptography backend
include_tasks: impl.yml
@@ -82,10 +71,4 @@
when: cryptography_version.stdout is version('3.0', '>=')
when: >-
(
(pyopenssl_version.stdout | default('0.0')) is version('0.15', '>=')
and
(pyopenssl_version.stdout | default('0.0')) is version('23.3.0', '<')
)
or
cryptography_version.stdout is version('3.0', '>=')

View File

@@ -81,8 +81,7 @@
- name: '({{ select_crypto_backend }}) Load "empty" file'
set_fact:
empty_contents: "{{ slurp.results[0].content | b64decode }}"
empty_expected_pyopenssl: "{{ (slurp.results[2].content | b64decode) ~ (slurp.results[1].content | b64decode) }}"
empty_expected_cryptography: "{{ (slurp.results[1].content | b64decode) ~ (slurp.results[2].content | b64decode) }}"
empty_expected: "{{ (slurp.results[1].content | b64decode) ~ (slurp.results[2].content | b64decode) }}"
- name: '({{ select_crypto_backend }}) Check "empty" file'
assert:
@@ -91,7 +90,7 @@
- p12_empty_idem is not changed
- p12_empty_concat_idem is not changed
- p12_empty_concat_content_idem is not changed
- (empty_contents == empty_expected_cryptography) or (empty_contents == empty_expected_pyopenssl and select_crypto_backend == 'pyopenssl')
- empty_contents == empty_expected
- name: '({{ select_crypto_backend }}) PKCS#12 with compatibility2022 settings'
when:

View File

@@ -91,9 +91,11 @@
- assert:
that:
- update_check is changed
- update_check.privatekey == 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
- update_check.privatekey != 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
- update_check.privatekey != result.privatekey
- update_check_return is changed
- update_check_return.privatekey == result.privatekey
- update_check_return.privatekey != 'VALUE_SPECIFIED_IN_NO_LOG_PARAMETER'
- update_check_return.privatekey != result.privatekey
- update is changed
- update.privatekey != result.privatekey
- update_info.public_data.size == default_rsa_key_size

View File

@@ -1,6 +0,0 @@
---
# Copyright (c) Ansible Project
# 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
has_pyopenssl: true

View File

@@ -1,9 +0,0 @@
---
# Copyright (c) Ansible Project
# 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
dependencies:
- setup_python_info
- setup_remote_constraints
- setup_pkg_mgr

View File

@@ -1,71 +0,0 @@
---
# Copyright (c) Ansible Project
# 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
####################################################################
# WARNING: These are designed specifically for Ansible tests #
# and should not be used as examples of how to write Ansible roles #
####################################################################
- name: Install from system packages
when: ansible_os_family != "Darwin" and target_system_python
block:
- name: Include OS-specific variables
include_vars: '{{ lookup("first_found", search) }}'
vars:
search:
files:
- '{{ ansible_distribution }}-{{ ansible_distribution_major_version }}.yml'
- '{{ ansible_distribution }}-{{ ansible_distribution_version }}.yml'
- '{{ ansible_distribution }}.yml'
- '{{ ansible_os_family }}.yml'
paths:
- vars
- when: has_pyopenssl
block:
- name: Install pyOpenSSL (Python 3 from system packages)
become: true
package:
name: '{{ pyopenssl_package_name_python3 }}'
when: ansible_python_version is version('3.0', '>=')
- name: Install pyOpenSSL (Python 2 from system packages)
become: true
package:
name: '{{ pyopenssl_package_name }}'
when: ansible_python_version is version('3.0', '<')
- name: Install from PyPi
when: ansible_os_family == "Darwin" or not target_system_python
block:
- name: Install pyOpenSSL (PyPi)
become: true
pip:
name: pyOpenSSL
state: "{{ 'latest' if not target_system_python_cannot_upgrade_cryptography else omit }}"
extra_args: "-c {{ remote_constraints }}"
- when: has_pyopenssl
block:
- name: Register pyOpenSSL version
command: "{{ ansible_python.executable }} -c 'import OpenSSL; print(OpenSSL.__version__)'"
register: pyopenssl_version
- name: Register pyOpenSSL debug details
command: "{{ ansible_python.executable }} -m OpenSSL.debug"
register: pyopenssl_debug_version
ignore_errors: true
# Depending on which pyOpenSSL version has been installed, it could be that cryptography has
# been upgraded to a newer version. Make sure to register cryptography_version another time here
# to avoid strange testing behavior due to wrong values of cryptography_version.
- name: Register cryptography version
command: "{{ ansible_python.executable }} -c 'import cryptography; print(cryptography.__version__)'"
register: cryptography_version
ignore_errors: true # in case cryptography was not installed, and setup_openssl hasn't been run before, ignore errors

View File

@@ -1,7 +0,0 @@
---
# Copyright (c) Ansible Project
# 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
pyopenssl_package_name: py-openssl
pyopenssl_package_name_python3: py3-openssl

View File

@@ -1,7 +0,0 @@
---
# Copyright (c) Ansible Project
# 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
pyopenssl_package_name: python-pyopenssl
pyopenssl_package_name_python3: python-pyopenssl

View File

@@ -1,7 +0,0 @@
---
# Copyright (c) Ansible Project
# 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
pyopenssl_package_name: python-openssl
pyopenssl_package_name_python3: python3-openssl

View File

@@ -1,7 +0,0 @@
---
# Copyright (c) Ansible Project
# 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
pyopenssl_package_name: py27-openssl
pyopenssl_package_name_python3: "py{{ ansible_python.version.major }}{{ ansible_python.version.minor }}-openssl"

View File

@@ -1,6 +0,0 @@
---
# Copyright (c) Ansible Project
# 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
has_pyopenssl: false

View File

@@ -1,7 +0,0 @@
---
# Copyright (c) Ansible Project
# 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
pyopenssl_package_name: pyOpenSSL
pyopenssl_package_name_python3: python3-pyOpenSSL

View File

@@ -1,7 +0,0 @@
---
# Copyright (c) Ansible Project
# 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
pyopenssl_package_name: python-pyOpenSSL
pyopenssl_package_name_python3: python3-pyOpenSSL

View File

@@ -5,6 +5,5 @@
dependencies:
- setup_acme
- setup_pyopenssl # needed for Ubuntu 16.04
- setup_remote_tmp_dir
- prepare_jinja2_compat

View File

@@ -68,20 +68,6 @@ def test_challenge_from_to_json():
assert challenge.token == "foo"
assert challenge.to_json() == data
data = {
"uri": "xxx",
"type": "type",
"status": "valid",
}
client.version = 1
challenge = Challenge.from_json(client, data)
assert challenge.data == data
assert challenge.type == "type"
assert challenge.url == "xxx"
assert challenge.status == "valid"
assert challenge.token is None
assert challenge.to_json() == data
def test_authorization_from_to_json():
client = MagicMock()
@@ -154,30 +140,6 @@ def test_authorization_from_to_json():
"wildcard": True,
}
client.version = 1
data = {
"challenges": [],
"identifier": {
"type": "dns",
"value": "example.com",
},
}
authz = Authorization.from_json(client, data, "xxx")
assert authz.url == "xxx"
assert authz.status == "pending"
assert authz.identifier == "example.com"
assert authz.identifier_type == "dns"
assert authz.challenges == []
assert authz.to_json() == {
"uri": "xxx",
"challenges": [],
"identifier": {
"type": "dns",
"value": "example.com",
},
}
def test_authorization_create_error():
client = MagicMock()