Reformat everything with black.

I had to undo the u string prefix removals to not drop Python 2 compatibility.
That's why black isn't enabled in antsibull-nox.toml yet.
This commit is contained in:
Felix Fontein
2025-04-28 09:51:33 +02:00
parent 04a0d38e3b
commit aec1826c34
118 changed files with 11780 additions and 7565 deletions

View File

@@ -188,73 +188,88 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main():
argument_spec = create_default_argspec()
argument_spec.update_argspec(
terms_agreed=dict(type='bool', default=False),
state=dict(type='str', required=True, choices=['absent', 'present', 'changed_key']),
allow_creation=dict(type='bool', default=True),
contact=dict(type='list', elements='str', default=[]),
new_account_key_src=dict(type='path'),
new_account_key_content=dict(type='str', no_log=True),
new_account_key_passphrase=dict(type='str', no_log=True),
external_account_binding=dict(type='dict', options=dict(
kid=dict(type='str', required=True),
alg=dict(type='str', required=True, choices=['HS256', 'HS384', 'HS512']),
key=dict(type='str', required=True, no_log=True),
))
terms_agreed=dict(type="bool", default=False),
state=dict(
type="str", required=True, choices=["absent", "present", "changed_key"]
),
allow_creation=dict(type="bool", default=True),
contact=dict(type="list", elements="str", default=[]),
new_account_key_src=dict(type="path"),
new_account_key_content=dict(type="str", no_log=True),
new_account_key_passphrase=dict(type="str", no_log=True),
external_account_binding=dict(
type="dict",
options=dict(
kid=dict(type="str", required=True),
alg=dict(
type="str", required=True, choices=["HS256", "HS384", "HS512"]
),
key=dict(type="str", required=True, no_log=True),
),
),
)
argument_spec.update(
mutually_exclusive=(
['new_account_key_src', 'new_account_key_content'],
),
mutually_exclusive=(["new_account_key_src", "new_account_key_content"],),
required_if=(
# Make sure that for state == changed_key, one of
# new_account_key_src and new_account_key_content are specified
['state', 'changed_key', ['new_account_key_src', 'new_account_key_content'], True],
[
"state",
"changed_key",
["new_account_key_src", "new_account_key_content"],
True,
],
),
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True)
if module.params['external_account_binding']:
if module.params["external_account_binding"]:
# Make sure padding is there
key = module.params['external_account_binding']['key']
key = module.params["external_account_binding"]["key"]
if len(key) % 4 != 0:
key = key + ('=' * (4 - (len(key) % 4)))
key = key + ("=" * (4 - (len(key) % 4)))
# Make sure key is Base64 encoded
try:
base64.urlsafe_b64decode(key)
except Exception as e:
module.fail_json(msg='Key for external_account_binding must be Base64 URL encoded (%s)' % e)
module.params['external_account_binding']['key'] = key
module.fail_json(
msg="Key for external_account_binding must be Base64 URL encoded (%s)"
% e
)
module.params["external_account_binding"]["key"] = key
try:
client = ACMEClient(module, backend)
account = ACMEAccount(client)
changed = False
state = module.params.get('state')
state = module.params.get("state")
diff_before = {}
diff_after = {}
if state == 'absent':
if state == "absent":
created, account_data = account.setup_account(allow_creation=False)
if account_data:
diff_before = dict(account_data)
diff_before['public_account_key'] = client.account_key_data['jwk']
diff_before["public_account_key"] = client.account_key_data["jwk"]
if created:
raise AssertionError('Unwanted account creation')
raise AssertionError("Unwanted account creation")
if account_data is not None:
# Account is not yet deactivated
if not module.check_mode:
# Deactivate it
payload = {
'status': 'deactivated'
}
payload = {"status": "deactivated"}
result, info = client.send_signed_request(
client.account_uri, payload, error_msg='Failed to deactivate account', expected_status_codes=[200])
client.account_uri,
payload,
error_msg="Failed to deactivate account",
expected_status_codes=[200],
)
changed = True
elif state == 'present':
allow_creation = module.params.get('allow_creation')
contact = [str(v) for v in module.params.get('contact')]
terms_agreed = module.params.get('terms_agreed')
external_account_binding = module.params.get('external_account_binding')
elif state == "present":
allow_creation = module.params.get("allow_creation")
contact = [str(v) for v in module.params.get("contact")]
terms_agreed = module.params.get("terms_agreed")
external_account_binding = module.params.get("external_account_binding")
created, account_data = account.setup_account(
contact,
terms_agreed=terms_agreed,
@@ -262,77 +277,87 @@ def main():
external_account_binding=external_account_binding,
)
if account_data is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.')
raise ModuleFailException(
msg="Account does not exist or is deactivated."
)
if created:
diff_before = {}
else:
diff_before = dict(account_data)
diff_before['public_account_key'] = client.account_key_data['jwk']
diff_before["public_account_key"] = client.account_key_data["jwk"]
updated = False
if not created:
updated, account_data = account.update_account(account_data, contact)
changed = created or updated
diff_after = dict(account_data)
diff_after['public_account_key'] = client.account_key_data['jwk']
elif state == 'changed_key':
diff_after["public_account_key"] = client.account_key_data["jwk"]
elif state == "changed_key":
# Parse new account key
try:
new_key_data = client.parse_key(
module.params.get('new_account_key_src'),
module.params.get('new_account_key_content'),
passphrase=module.params.get('new_account_key_passphrase'),
module.params.get("new_account_key_src"),
module.params.get("new_account_key_content"),
passphrase=module.params.get("new_account_key_passphrase"),
)
except KeyParsingError as e:
raise ModuleFailException("Error while parsing new account key: {msg}".format(msg=e.msg))
raise ModuleFailException(
"Error while parsing new account key: {msg}".format(msg=e.msg)
)
# Verify that the account exists and has not been deactivated
created, account_data = account.setup_account(allow_creation=False)
if created:
raise AssertionError('Unwanted account creation')
raise AssertionError("Unwanted account creation")
if account_data is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.')
raise ModuleFailException(
msg="Account does not exist or is deactivated."
)
diff_before = dict(account_data)
diff_before['public_account_key'] = client.account_key_data['jwk']
diff_before["public_account_key"] = client.account_key_data["jwk"]
# Now we can start the account key rollover
if not module.check_mode:
# Compose inner signed message
# https://tools.ietf.org/html/rfc8555#section-7.3.5
url = client.directory['keyChange']
url = client.directory["keyChange"]
protected = {
"alg": new_key_data['alg'],
"jwk": new_key_data['jwk'],
"alg": new_key_data["alg"],
"jwk": new_key_data["jwk"],
"url": url,
}
payload = {
"account": client.account_uri,
"newKey": new_key_data['jwk'], # specified in draft 12 and older
"newKey": new_key_data["jwk"], # specified in draft 12 and older
"oldKey": client.account_jwk, # specified in draft 13 and newer
}
data = client.sign_request(protected, payload, new_key_data)
# Send request and verify result
result, info = client.send_signed_request(
url, data, error_msg='Failed to rollover account key', expected_status_codes=[200])
url,
data,
error_msg="Failed to rollover account key",
expected_status_codes=[200],
)
if module._diff:
client.account_key_data = new_key_data
client.account_jws_header['alg'] = new_key_data['alg']
client.account_jws_header["alg"] = new_key_data["alg"]
diff_after = account.get_account_data()
elif module._diff:
# Kind of fake diff_after
diff_after = dict(diff_before)
diff_after['public_account_key'] = new_key_data['jwk']
diff_after["public_account_key"] = new_key_data["jwk"]
changed = True
result = {
'changed': changed,
'account_uri': client.account_uri,
"changed": changed,
"account_uri": client.account_uri,
}
if module._diff:
result['diff'] = {
'before': diff_before,
'after': diff_after,
result["diff"] = {
"before": diff_before,
"after": diff_after,
}
module.exit_json(**result)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -226,24 +226,30 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import
def get_orders_list(module, client, orders_url):
'''
"""
Retrieves orders list (handles pagination).
'''
"""
orders = []
while orders_url:
# Get part of orders list
res, info = client.get_request(orders_url, parse_json_result=True, fail_on_error=True)
if not res.get('orders'):
res, info = client.get_request(
orders_url, parse_json_result=True, fail_on_error=True
)
if not res.get("orders"):
if orders:
module.warn('When retrieving orders list part {0}, got empty result list'.format(orders_url))
module.warn(
"When retrieving orders list part {0}, got empty result list".format(
orders_url
)
)
break
# Add order URLs to result list
orders.extend(res['orders'])
orders.extend(res["orders"])
# Extract URL of next part of results list
new_orders_url = []
def f(link, relation):
if relation == 'next':
if relation == "next":
new_orders_url.append(link)
process_links(info, f)
@@ -256,16 +262,18 @@ def get_orders_list(module, client, orders_url):
def get_order(client, order_url):
'''
"""
Retrieve order data.
'''
"""
return client.get_request(order_url, parse_json_result=True, fail_on_error=True)[0]
def main():
argument_spec = create_default_argspec()
argument_spec.update_argspec(
retrieve_orders=dict(type='str', default='ignore', choices=['ignore', 'url_list', 'object_list']),
retrieve_orders=dict(
type="str", default="ignore", choices=["ignore", "url_list", "object_list"]
),
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True)
@@ -280,28 +288,31 @@ def main():
remove_account_uri_if_not_exists=True,
)
if created:
raise AssertionError('Unwanted account creation')
raise AssertionError("Unwanted account creation")
result = {
'changed': False,
'exists': client.account_uri is not None,
'account_uri': client.account_uri,
"changed": False,
"exists": client.account_uri is not None,
"account_uri": client.account_uri,
}
if client.account_uri is not None:
# Make sure promised data is there
if 'contact' not in account_data:
account_data['contact'] = []
account_data['public_account_key'] = client.account_key_data['jwk']
result['account'] = account_data
if "contact" not in account_data:
account_data["contact"] = []
account_data["public_account_key"] = client.account_key_data["jwk"]
result["account"] = account_data
# Retrieve orders list
if account_data.get('orders') and module.params['retrieve_orders'] != 'ignore':
orders = get_orders_list(module, client, account_data['orders'])
result['order_uris'] = orders
if module.params['retrieve_orders'] == 'object_list':
result['orders'] = [get_order(client, order) for order in orders]
if (
account_data.get("orders")
and module.params["retrieve_orders"] != "ignore"
):
orders = get_orders_list(module, client, account_data["orders"])
result["order_uris"] = orders
if module.params["retrieve_orders"] == "object_list":
result["orders"] = [get_order(client, order) for order in orders]
module.exit_json(**result)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -112,16 +112,12 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main():
argument_spec = create_default_argspec(with_account=False)
argument_spec.update_argspec(
certificate_path=dict(type='path'),
certificate_content=dict(type='str'),
certificate_path=dict(type="path"),
certificate_content=dict(type="str"),
)
argument_spec.update(
required_one_of=(
['certificate_path', 'certificate_content'],
),
mutually_exclusive=(
['certificate_path', 'certificate_content'],
),
required_one_of=(["certificate_path", "certificate_content"],),
mutually_exclusive=(["certificate_path", "certificate_content"],),
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True)
@@ -129,10 +125,12 @@ def main():
try:
client = ACMEClient(module, backend)
if not client.directory.has_renewal_info_endpoint():
module.fail_json(msg='The ACME endpoint does not support ACME Renewal Information retrieval')
module.fail_json(
msg="The ACME endpoint does not support ACME Renewal Information retrieval"
)
renewal_info = client.get_renewal_info(
cert_filename=module.params['certificate_path'],
cert_content=module.params['certificate_content'],
cert_filename=module.params["certificate_path"],
cert_content=module.params["certificate_content"],
include_retry_after=True,
)
module.exit_json(renewal_info=renewal_info)
@@ -140,5 +138,5 @@ def main():
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -600,76 +600,94 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import
)
NO_CHALLENGE = 'no challenge'
NO_CHALLENGE = "no challenge"
class ACMECertificateClient(object):
'''
"""
ACME client class. Uses an ACME account object and a CSR to
start and validate ACME challenges and download the respective
certificates.
'''
"""
def __init__(self, module, backend):
self.module = module
self.version = module.params['acme_version']
self.challenge = module.params['challenge']
self.version = module.params["acme_version"]
self.challenge = module.params["challenge"]
# We use None instead of a magic string for 'no challenge'
if self.challenge == NO_CHALLENGE:
self.challenge = None
self.csr = module.params['csr']
self.csr_content = module.params['csr_content']
self.dest = module.params.get('dest')
self.fullchain_dest = module.params.get('fullchain_dest')
self.chain_dest = module.params.get('chain_dest')
self.csr = module.params["csr"]
self.csr_content = module.params["csr_content"]
self.dest = module.params.get("dest")
self.fullchain_dest = module.params.get("fullchain_dest")
self.chain_dest = module.params.get("chain_dest")
self.client = ACMEClient(module, backend)
self.account = ACMEAccount(self.client)
self.directory = self.client.directory
self.data = module.params['data']
self.data = module.params["data"]
self.authorizations = None
self.cert_days = -1
self.order = None
self.order_uri = self.data.get('order_uri') if self.data else None
self.order_uri = self.data.get("order_uri") if self.data else None
self.all_chains = None
self.select_chain_matcher = []
self.include_renewal_cert_id = module.params['include_renewal_cert_id']
self.profile = module.params['profile']
self.order_creation_error_strategy = module.params['order_creation_error_strategy']
self.order_creation_max_retries = module.params['order_creation_max_retries']
self.include_renewal_cert_id = module.params["include_renewal_cert_id"]
self.profile = module.params["profile"]
self.order_creation_error_strategy = module.params[
"order_creation_error_strategy"
]
self.order_creation_max_retries = module.params["order_creation_max_retries"]
if self.module.params['select_chain']:
for criterium_idx, criterium in enumerate(self.module.params['select_chain']):
if self.module.params["select_chain"]:
for criterium_idx, criterium in enumerate(
self.module.params["select_chain"]
):
try:
self.select_chain_matcher.append(
self.client.backend.create_chain_matcher(
Criterium(criterium, index=criterium_idx)))
Criterium(criterium, index=criterium_idx)
)
)
except ValueError as exc:
self.module.warn('Error while parsing criterium: {error}. Ignoring criterium.'.format(error=exc))
self.module.warn(
"Error while parsing criterium: {error}. Ignoring criterium.".format(
error=exc
)
)
if self.profile is not None:
meta_profiles = (self.directory.get('meta') or {}).get('profiles') or {}
meta_profiles = (self.directory.get("meta") or {}).get("profiles") or {}
if not meta_profiles:
raise ModuleFailException(msg='The ACME CA does not support profiles.')
raise ModuleFailException(msg="The ACME CA does not support profiles.")
if self.profile not in meta_profiles:
raise ModuleFailException(msg='The ACME CA does not support selected profile {0!r}.'.format(self.profile))
raise ModuleFailException(
msg="The ACME CA does not support selected profile {0!r}.".format(
self.profile
)
)
# Make sure account exists
modify_account = module.params['modify_account']
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'])
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'),
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.')
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)
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
@@ -683,13 +701,15 @@ class ACMECertificateClient(object):
raise ModuleFailException("CSR %s not found" % (self.csr))
# Extract list of identifiers from CSR
self.identifiers = self.client.backend.get_ordered_csr_identifiers(csr_filename=self.csr, csr_content=self.csr_content)
self.identifiers = self.client.backend.get_ordered_csr_identifiers(
csr_filename=self.csr, csr_content=self.csr_content
)
def is_first_step(self):
'''
"""
Return True if this is the first execution of this module, i.e. if a
sufficient data object from a first run has not been provided.
'''
"""
if self.data is None:
return True
if self.version == 1:
@@ -701,32 +721,34 @@ class ACMECertificateClient(object):
return self.order_uri is None
def _get_cert_info_or_none(self):
if self.module.params.get('dest'):
filename = self.module.params['dest']
if self.module.params.get("dest"):
filename = self.module.params["dest"]
else:
filename = self.module.params['fullchain_dest']
filename = self.module.params["fullchain_dest"]
if not os.path.exists(filename):
return None
return self.client.backend.get_cert_information(cert_filename=filename)
def start_challenges(self):
'''
"""
Create new authorizations for all identifiers of the CSR,
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!')
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
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())
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:
@@ -750,39 +772,46 @@ class ACMECertificateClient(object):
self.changed = True
def get_challenges_data(self, first_step):
'''
"""
Get challenge details for the chosen challenge type.
Return a tuple of generic challenge details, and specialized DNS challenge details.
'''
"""
# Get general challenge data
data = {}
for type_identifier, authz in self.authorizations.items():
identifier_type, identifier = split_identifier(type_identifier)
# Skip valid authentications: their challenges are already valid
# and do not need to be returned
if authz.status == 'valid':
if authz.status == "valid":
continue
# We drop the type from the key to preserve backwards compatibility
data[authz.identifier] = authz.get_challenge_data(self.client)
if first_step and self.challenge is not None and self.challenge not in data[authz.identifier]:
raise ModuleFailException("Found no challenge of type '{0}' for identifier {1}!".format(
self.challenge, type_identifier))
if (
first_step
and self.challenge is not None
and self.challenge not in data[authz.identifier]
):
raise ModuleFailException(
"Found no challenge of type '{0}' for identifier {1}!".format(
self.challenge, type_identifier
)
)
# Get DNS challenge data
data_dns = {}
if self.challenge == 'dns-01':
if self.challenge == "dns-01":
for identifier, challenges in data.items():
if self.challenge in challenges:
values = data_dns.get(challenges[self.challenge]['record'])
values = data_dns.get(challenges[self.challenge]["record"])
if values is None:
values = []
data_dns[challenges[self.challenge]['record']] = values
values.append(challenges[self.challenge]['resource_value'])
data_dns[challenges[self.challenge]["record"]] = values
values.append(challenges[self.challenge]["resource_value"])
return data, data_dns
def finish_challenges(self):
'''
"""
Verify challenges for all identifiers of the CSR.
'''
"""
self.authorizations = {}
# Step 1: obtain challenge information
@@ -791,7 +820,9 @@ class ACMECertificateClient(object):
# 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
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.
@@ -802,12 +833,12 @@ class ACMECertificateClient(object):
# Step 2: validate pending challenges
authzs_to_wait_for = []
for type_identifier, authz in self.authorizations.items():
if authz.status == 'pending':
if authz.status == "pending":
if self.challenge is not None:
authz.call_validate(self.client, self.challenge, wait=False)
authzs_to_wait_for.append(authz)
# If there is no challenge, we must check whether the authz is valid
elif authz.status != 'valid':
elif authz.status != "valid":
authz.raise_error(
'Status is not "valid", even though no challenge should be necessary',
module=self.client.module,
@@ -823,7 +854,11 @@ class ACMECertificateClient(object):
try:
alt_cert = CertificateChain.download(self.client, alternate)
except ModuleFailException as e:
self.module.warn('Error while downloading alternative certificate {0}: {1}'.format(alternate, e))
self.module.warn(
"Error while downloading alternative certificate {0}: {1}".format(
alternate, e
)
)
continue
alternate_chains.append(alt_cert)
return alternate_chains
@@ -832,35 +867,52 @@ class ACMECertificateClient(object):
for criterium_idx, matcher in enumerate(self.select_chain_matcher):
for chain in chains:
if matcher.match(chain):
self.module.debug('Found matching chain for criterium {0}'.format(criterium_idx))
self.module.debug(
"Found matching chain for criterium {0}".format(criterium_idx)
)
return chain
return None
def get_certificate(self):
'''
"""
Request a new certificate and write it to the destination file.
First verifies whether all authorizations are valid; if not, aborts
with an error.
'''
"""
for identifier_type, identifier in self.identifiers:
authz = self.authorizations.get(normalize_combined_identifier(combine_identifier(identifier_type, identifier)))
authz = self.authorizations.get(
normalize_combined_identifier(
combine_identifier(identifier_type, identifier)
)
)
if authz is None:
raise ModuleFailException('Found no authorization information for "{identifier}"!'.format(
identifier=combine_identifier(identifier_type, identifier)))
if authz.status != 'valid':
authz.raise_error('Status is "{status}" and not "valid"'.format(status=authz.status), module=self.module)
raise ModuleFailException(
'Found no authorization information for "{identifier}"!'.format(
identifier=combine_identifier(identifier_type, identifier)
)
)
if authz.status != "valid":
authz.raise_error(
'Status is "{status}" and not "valid"'.format(status=authz.status),
module=self.module,
)
if self.version == 1:
cert = retrieve_acme_v1_certificate(self.client, pem_to_der(self.csr, self.csr_content))
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:
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']:
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())
@@ -871,89 +923,122 @@ class ACMECertificateClient(object):
if matching_chain:
cert = matching_chain
else:
self.module.debug('Found no matching alternative chain')
self.module.debug("Found no matching alternative chain")
if cert.cert is not None:
pem_cert = cert.cert
chain = cert.chain
if self.dest and write_file(self.module, self.dest, pem_cert.encode('utf8')):
if self.dest and write_file(
self.module, self.dest, pem_cert.encode("utf8")
):
self.cert_days = self.client.backend.get_cert_days(self.dest)
self.changed = True
if self.fullchain_dest and write_file(self.module, self.fullchain_dest, (pem_cert + "\n".join(chain)).encode('utf8')):
if self.fullchain_dest and write_file(
self.module,
self.fullchain_dest,
(pem_cert + "\n".join(chain)).encode("utf8"),
):
self.cert_days = self.client.backend.get_cert_days(self.fullchain_dest)
self.changed = True
if self.chain_dest and write_file(self.module, self.chain_dest, ("\n".join(chain)).encode('utf8')):
if self.chain_dest and write_file(
self.module, self.chain_dest, ("\n".join(chain)).encode("utf8")
):
self.changed = True
def deactivate_authzs(self):
'''
"""
Deactivates all valid authz's. Does not raise exceptions.
https://community.letsencrypt.org/t/authorization-deactivation/19860/2
https://tools.ietf.org/html/rfc8555#section-7.5.2
'''
"""
for authz in self.authorizations.values():
try:
authz.deactivate(self.client)
except Exception:
# ignore errors
pass
if authz.status != 'deactivated':
self.module.warn(warning='Could not deactivate authz object {0}.'.format(authz.url))
if authz.status != "deactivated":
self.module.warn(
warning="Could not deactivate authz object {0}.".format(authz.url)
)
def main():
argument_spec = create_default_argspec(with_certificate=True)
argument_spec.argument_spec['csr']['aliases'] = ['src']
argument_spec.argument_spec["csr"]["aliases"] = ["src"]
argument_spec.update_argspec(
modify_account=dict(type='bool', default=True),
account_email=dict(type='str'),
agreement=dict(type='str'),
terms_agreed=dict(type='bool', default=False),
challenge=dict(type='str', default='http-01', choices=['http-01', 'dns-01', 'tls-alpn-01', NO_CHALLENGE]),
data=dict(type='dict'),
dest=dict(type='path', aliases=['cert']),
fullchain_dest=dict(type='path', aliases=['fullchain']),
chain_dest=dict(type='path', aliases=['chain']),
remaining_days=dict(type='int', default=10),
deactivate_authzs=dict(type='bool', default=False),
force=dict(type='bool', default=False),
retrieve_all_alternates=dict(type='bool', default=False),
select_chain=dict(type='list', elements='dict', options=dict(
test_certificates=dict(type='str', default='all', choices=['first', 'last', 'all']),
issuer=dict(type='dict'),
subject=dict(type='dict'),
subject_key_identifier=dict(type='str'),
authority_key_identifier=dict(type='str'),
)),
include_renewal_cert_id=dict(type='str', choices=['never', 'when_ari_supported', 'always'], default='never'),
profile=dict(type='str'),
order_creation_error_strategy=dict(type='str', default='auto', choices=['auto', 'always', 'fail', 'retry_without_replaces_cert_id']),
order_creation_max_retries=dict(type='int', default=3),
modify_account=dict(type="bool", default=True),
account_email=dict(type="str"),
agreement=dict(type="str"),
terms_agreed=dict(type="bool", default=False),
challenge=dict(
type="str",
default="http-01",
choices=["http-01", "dns-01", "tls-alpn-01", NO_CHALLENGE],
),
data=dict(type="dict"),
dest=dict(type="path", aliases=["cert"]),
fullchain_dest=dict(type="path", aliases=["fullchain"]),
chain_dest=dict(type="path", aliases=["chain"]),
remaining_days=dict(type="int", default=10),
deactivate_authzs=dict(type="bool", default=False),
force=dict(type="bool", default=False),
retrieve_all_alternates=dict(type="bool", default=False),
select_chain=dict(
type="list",
elements="dict",
options=dict(
test_certificates=dict(
type="str", default="all", choices=["first", "last", "all"]
),
issuer=dict(type="dict"),
subject=dict(type="dict"),
subject_key_identifier=dict(type="str"),
authority_key_identifier=dict(type="str"),
),
),
include_renewal_cert_id=dict(
type="str",
choices=["never", "when_ari_supported", "always"],
default="never",
),
profile=dict(type="str"),
order_creation_error_strategy=dict(
type="str",
default="auto",
choices=["auto", "always", "fail", "retry_without_replaces_cert_id"],
),
order_creation_max_retries=dict(type="int", default=3),
)
argument_spec.update(
required_one_of=[
['dest', 'fullchain_dest'],
["dest", "fullchain_dest"],
],
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, False)
try:
if module.params.get('dest'):
cert_days = backend.get_cert_days(module.params['dest'])
if module.params.get("dest"):
cert_days = backend.get_cert_days(module.params["dest"])
else:
cert_days = backend.get_cert_days(module.params['fullchain_dest'])
cert_days = backend.get_cert_days(module.params["fullchain_dest"])
if module.params['force'] or cert_days < module.params['remaining_days']:
if module.params["force"] or cert_days < module.params["remaining_days"]:
# If checkmode is active, base the changed state solely on the status
# of the certificate file as all other actions (accessing an account, checking
# the authorization status...) would lead to potential changes of the current
# state
if module.check_mode:
module.exit_json(changed=True, authorizations={}, challenge_data={}, cert_days=cert_days)
module.exit_json(
changed=True,
authorizations={},
challenge_data={},
cert_days=cert_days,
)
else:
client = ACMECertificateClient(module, backend)
client.cert_days = cert_days
@@ -968,9 +1053,9 @@ def main():
client.finish_challenges()
client.get_certificate()
if client.all_chains is not None:
other['all_chains'] = client.all_chains
other["all_chains"] = client.all_chains
finally:
if module.params['deactivate_authzs']:
if module.params["deactivate_authzs"]:
client.deactivate_authzs()
data, data_dns = client.get_challenges_data(first_step=is_first_step)
auths = dict()
@@ -994,5 +1079,5 @@ def main():
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -73,11 +73,11 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.orders impor
def main():
argument_spec = create_default_argspec()
argument_spec.update_argspec(
order_uri=dict(type='str', required=True),
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')
if module.params["acme_version"] == 1:
module.fail_json("The module does not support acme_version=1")
backend = create_backend(module, False)
@@ -87,9 +87,9 @@ def main():
dummy, account_data = account.setup_account(allow_creation=False)
if account_data is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.')
raise ModuleFailException(msg="Account does not exist or is deactivated.")
order = Order.from_url(client, module.params['order_uri'])
order = Order.from_url(client, module.params["order_uri"])
order.load_authorizations(client)
changed = False
@@ -104,13 +104,15 @@ def main():
except Exception:
# ignore errors
pass
if authz.status != 'deactivated':
module.warn(warning='Could not deactivate authz object {0}.'.format(authz.url))
if authz.status != "deactivated":
module.warn(
warning="Could not deactivate authz object {0}.".format(authz.url)
)
module.exit_json(changed=changed)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -11,7 +11,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = """
module: acme_certificate_order_create
author: Felix Fontein (@felixfontein)
version_added: 2.24.0
@@ -158,9 +158,9 @@ options:
for at most the specified amount of times.
type: int
default: 3
'''
"""
EXAMPLES = r'''
EXAMPLES = r"""
---
### Example with HTTP-01 challenge ###
@@ -253,9 +253,9 @@ EXAMPLES = r'''
cert_dest: /etc/httpd/ssl/sample.com.crt
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
'''
"""
RETURN = '''
RETURN = """
challenge_data:
description:
- For every identifier, provides the challenge information.
@@ -377,7 +377,7 @@ account_uri:
description: ACME account URI.
returned: success
type: str
'''
"""
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
@@ -394,37 +394,51 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main():
argument_spec = create_default_argspec(with_certificate=True)
argument_spec.update_argspec(
deactivate_authzs=dict(type='bool', default=True),
replaces_cert_id=dict(type='str'),
profile=dict(type='str'),
order_creation_error_strategy=dict(type='str', default='auto', choices=['auto', 'always', 'fail', 'retry_without_replaces_cert_id']),
order_creation_max_retries=dict(type='int', default=3),
deactivate_authzs=dict(type="bool", default=True),
replaces_cert_id=dict(type="str"),
profile=dict(type="str"),
order_creation_error_strategy=dict(
type="str",
default="auto",
choices=["auto", "always", "fail", "retry_without_replaces_cert_id"],
),
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')
if module.params["acme_version"] == 1:
module.fail_json("The module does not support acme_version=1")
backend = create_backend(module, False)
try:
client = ACMECertificateClient(module, backend)
profile = module.params['profile']
profile = module.params["profile"]
if profile is not None:
meta_profiles = (client.client.directory.get('meta') or {}).get('profiles') or {}
meta_profiles = (client.client.directory.get("meta") or {}).get(
"profiles"
) or {}
if not meta_profiles:
raise ModuleFailException(msg='The ACME CA does not support profiles. Please omit the "profile" option.')
raise ModuleFailException(
msg='The ACME CA does not support profiles. Please omit the "profile" option.'
)
if profile not in meta_profiles:
raise ModuleFailException(msg='The ACME CA does not support selected profile {0!r}.'.format(profile))
raise ModuleFailException(
msg="The ACME CA does not support selected profile {0!r}.".format(
profile
)
)
order = None
done = False
try:
order = client.create_order(replaces_cert_id=module.params['replaces_cert_id'], profile=profile)
order = client.create_order(
replaces_cert_id=module.params["replaces_cert_id"], profile=profile
)
client.check_that_authorizations_can_be_used(order)
done = True
finally:
if module.params['deactivate_authzs'] and order and not done:
if module.params["deactivate_authzs"] and order and not done:
client.deactivate_authzs(order)
data, data_dns = client.get_challenges_data(order)
module.exit_json(
@@ -438,5 +452,5 @@ def main():
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -11,7 +11,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = """
module: acme_certificate_order_finalize
author: Felix Fontein (@felixfontein)
version_added: 2.24.0
@@ -170,9 +170,9 @@ options:
- "The identifier must be of the form
V(C4:A7:B1:A4:7B:2C:71:FA:DB:E1:4B:90:75:FF:C4:15:60:85:89:10)."
type: str
'''
"""
EXAMPLES = r'''
EXAMPLES = r"""
---
### Example with HTTP-01 challenge ###
@@ -265,9 +265,9 @@ EXAMPLES = r'''
cert_dest: /etc/httpd/ssl/sample.com.crt
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
'''
"""
RETURN = '''
RETURN = """
account_uri:
description: ACME account URI.
returned: success
@@ -323,7 +323,7 @@ selected_chain:
as concatenated PEM certificates.
type: str
returned: always
'''
"""
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
@@ -340,29 +340,39 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main():
argument_spec = create_default_argspec(with_certificate=True)
argument_spec.update_argspec(
order_uri=dict(type='str', required=True),
cert_dest=dict(type='path'),
fullchain_dest=dict(type='path'),
chain_dest=dict(type='path'),
deactivate_authzs=dict(type='str', default='always', choices=['never', 'always', 'on_error', 'on_success']),
retrieve_all_alternates=dict(type='bool', default=False),
select_chain=dict(type='list', elements='dict', options=dict(
test_certificates=dict(type='str', default='all', choices=['first', 'last', 'all']),
issuer=dict(type='dict'),
subject=dict(type='dict'),
subject_key_identifier=dict(type='str'),
authority_key_identifier=dict(type='str'),
)),
order_uri=dict(type="str", required=True),
cert_dest=dict(type="path"),
fullchain_dest=dict(type="path"),
chain_dest=dict(type="path"),
deactivate_authzs=dict(
type="str",
default="always",
choices=["never", "always", "on_error", "on_success"],
),
retrieve_all_alternates=dict(type="bool", default=False),
select_chain=dict(
type="list",
elements="dict",
options=dict(
test_certificates=dict(
type="str", default="all", choices=["first", "last", "all"]
),
issuer=dict(type="dict"),
subject=dict(type="dict"),
subject_key_identifier=dict(type="str"),
authority_key_identifier=dict(type="str"),
),
),
)
module = argument_spec.create_ansible_module()
if module.params['acme_version'] == 1:
module.fail_json('The module does not support acme_version=1')
if module.params["acme_version"] == 1:
module.fail_json("The module does not support acme_version=1")
backend = create_backend(module, False)
try:
client = ACMECertificateClient(module, backend)
select_chain_matcher = client.parse_select_chain(module.params['select_chain'])
select_chain_matcher = client.parse_select_chain(module.params["select_chain"])
other = dict()
done = False
order = None
@@ -370,9 +380,12 @@ def main():
# Step 1: load order
order = client.load_order()
download_all_chains = len(select_chain_matcher) > 0 or module.params['retrieve_all_alternates']
download_all_chains = (
len(select_chain_matcher) > 0
or module.params["retrieve_all_alternates"]
)
changed = False
if order.status == 'valid':
if order.status == "valid":
# Step 2 and 3: download certificate(s) and chain(s)
cert, alternate_chains = client.download_certificate(
order,
@@ -395,34 +408,36 @@ def main():
# Step 4: pick chain, write certificates, and provide return values
if alternate_chains is not None:
# Prepare return value for all alternate chains
if module.params['retrieve_all_alternates']:
if module.params["retrieve_all_alternates"]:
all_chains = [cert.to_json()]
for alt_chain in alternate_chains:
all_chains.append(alt_chain.to_json())
other['all_chains'] = all_chains
other["all_chains"] = all_chains
# Try to select alternate chain depending on criteria
if select_chain_matcher:
matching_chain = client.find_matching_chain([cert] + alternate_chains, select_chain_matcher)
matching_chain = client.find_matching_chain(
[cert] + alternate_chains, select_chain_matcher
)
if matching_chain:
cert = matching_chain
else:
module.debug('Found no matching alternative chain')
module.debug("Found no matching alternative chain")
if client.write_cert_chain(
cert,
cert_dest=module.params['cert_dest'],
fullchain_dest=module.params['fullchain_dest'],
chain_dest=module.params['chain_dest'],
cert_dest=module.params["cert_dest"],
fullchain_dest=module.params["fullchain_dest"],
chain_dest=module.params["chain_dest"],
):
changed = True
done = True
finally:
if (
module.params['deactivate_authzs'] == 'always' or
(module.params['deactivate_authzs'] == 'on_success' and done) or
(module.params['deactivate_authzs'] == 'on_error' and not done)
module.params["deactivate_authzs"] == "always"
or (module.params["deactivate_authzs"] == "on_success" and done)
or (module.params["deactivate_authzs"] == "on_error" and not done)
):
if order:
client.deactivate_authzs(order)
@@ -436,5 +451,5 @@ def main():
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -11,7 +11,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = """
module: acme_certificate_order_info
author: Felix Fontein (@felixfontein)
version_added: 2.24.0
@@ -57,9 +57,9 @@ options:
- The order URI provided by RV(community.crypto.acme_certificate_order_create#module:order_uri).
type: str
required: true
'''
"""
EXAMPLES = r'''
EXAMPLES = r"""
---
- name: Create a challenge for sample.com using a account key from a variable
community.crypto.acme_certificate_order_create:
@@ -76,9 +76,9 @@ EXAMPLES = r'''
- name: Show information
ansible.builtin.debug:
var: order_info
'''
"""
RETURN = '''
RETURN = """
account_uri:
description: ACME account URI.
returned: success
@@ -360,7 +360,7 @@ authorizations_by_status:
type: list
elements: str
returned: always
'''
"""
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
@@ -377,11 +377,11 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main():
argument_spec = create_default_argspec(with_certificate=False)
argument_spec.update_argspec(
order_uri=dict(type='str', required=True),
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')
if module.params["acme_version"] == 1:
module.fail_json("The module does not support acme_version=1")
backend = create_backend(module, False)
@@ -390,12 +390,12 @@ def main():
order = client.load_order()
authorizations_by_identifier = dict()
authorizations_by_status = {
'pending': [],
'invalid': [],
'valid': [],
'revoked': [],
'deactivated': [],
'expired': [],
"pending": [],
"invalid": [],
"valid": [],
"revoked": [],
"deactivated": [],
"expired": [],
}
for identifier, authz in order.authorizations.items():
authorizations_by_identifier[identifier] = authz.to_json()
@@ -412,5 +412,5 @@ def main():
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -11,7 +11,7 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = '''
DOCUMENTATION = """
module: acme_certificate_order_validate
author: Felix Fontein (@felixfontein)
version_added: 2.24.0
@@ -95,9 +95,9 @@ options:
concern."
type: bool
default: true
'''
"""
EXAMPLES = r'''
EXAMPLES = r"""
---
### Example with HTTP-01 challenge ###
@@ -190,9 +190,9 @@ EXAMPLES = r'''
cert_dest: /etc/httpd/ssl/sample.com.crt
fullchain_dest: /etc/httpd/ssl/sample.com-fullchain.crt
chain_dest: /etc/httpd/ssl/sample.com-intermediate.crt
'''
"""
RETURN = '''
RETURN = """
account_uri:
description: ACME account URI.
returned: success
@@ -235,7 +235,7 @@ validating_challenges:
- The URL of the challenge object.
type: str
returned: always
'''
"""
from ansible_collections.community.crypto.plugins.module_utils.acme.acme import (
create_backend,
@@ -252,13 +252,13 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main():
argument_spec = create_default_argspec(with_certificate=False)
argument_spec.update_argspec(
order_uri=dict(type='str', required=True),
challenge=dict(type='str', choices=['http-01', 'dns-01', 'tls-alpn-01']),
deactivate_authzs=dict(type='bool', default=True),
order_uri=dict(type="str", required=True),
challenge=dict(type="str", choices=["http-01", "dns-01", "tls-alpn-01"]),
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')
if module.params["acme_version"] == 1:
module.fail_json("The module does not support acme_version=1")
backend = create_backend(module, False)
@@ -277,34 +277,43 @@ def main():
# Step 3: figure out challenges to use
challenges = {}
for authz in pending_authzs:
challenges[authz.combined_identifier] = module.params['challenge']
challenges[authz.combined_identifier] = module.params["challenge"]
missing_challenge_authzs = [k for k, v in challenges.items() if v is None]
if missing_challenge_authzs:
raise ModuleFailException(
'The challenge parameter must be supplied if there are pending authorizations.'
' The following authorizations are pending: {missing_challenge_authzs}'.format(
missing_challenge_authzs=', '.join(sorted(missing_challenge_authzs)),
"The challenge parameter must be supplied if there are pending authorizations."
" The following authorizations are pending: {missing_challenge_authzs}".format(
missing_challenge_authzs=", ".join(
sorted(missing_challenge_authzs)
),
)
)
bad_challenge_authzs = [
authz.combined_identifier for authz in pending_authzs
authz.combined_identifier
for authz in pending_authzs
if authz.find_challenge(challenges[authz.combined_identifier]) is None
]
if bad_challenge_authzs:
raise ModuleFailException(
'The following authorizations do not support the selected challenges: {authz_challenges_pairs}'.format(
authz_challenges_pairs=', '.join(sorted(
'{authz} with {challenge}'.format(authz=authz, challenge=challenges[authz])
for authz in bad_challenge_authzs
)),
"The following authorizations do not support the selected challenges: {authz_challenges_pairs}".format(
authz_challenges_pairs=", ".join(
sorted(
"{authz} with {challenge}".format(
authz=authz, challenge=challenges[authz]
)
for authz in bad_challenge_authzs
)
),
)
)
really_pending_authzs = [
authz for authz in pending_authzs
if authz.find_challenge(challenges[authz.combined_identifier]).status == 'pending'
authz
for authz in pending_authzs
if authz.find_challenge(challenges[authz.combined_identifier]).status
== "pending"
]
# Step 4: validate pending authorizations
@@ -316,7 +325,7 @@ def main():
done = True
finally:
if order and module.params['deactivate_authzs'] and not done:
if order and module.params["deactivate_authzs"] and not done:
client.deactivate_authzs(order)
module.exit_json(
changed=len(authzs_with_challenges_to_wait_for) > 0,
@@ -336,5 +345,5 @@ def main():
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -183,110 +183,144 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import
def main():
argument_spec = create_default_argspec(with_account=False)
argument_spec.update_argspec(
certificate_path=dict(type='path'),
certificate_content=dict(type='str'),
use_ari=dict(type='bool', default=True),
ari_algorithm=dict(type='str', choices=['standard', 'start'], default='standard'),
remaining_days=dict(type='int'),
remaining_percentage=dict(type='float'),
now=dict(type='str'),
treat_parsing_error_as_non_existing=dict(type='bool', default=False),
certificate_path=dict(type="path"),
certificate_content=dict(type="str"),
use_ari=dict(type="bool", default=True),
ari_algorithm=dict(
type="str", choices=["standard", "start"], default="standard"
),
remaining_days=dict(type="int"),
remaining_percentage=dict(type="float"),
now=dict(type="str"),
treat_parsing_error_as_non_existing=dict(type="bool", default=False),
)
argument_spec.update(
mutually_exclusive=(
['certificate_path', 'certificate_content'],
),
mutually_exclusive=(["certificate_path", "certificate_content"],),
)
module = argument_spec.create_ansible_module(supports_check_mode=True)
backend = create_backend(module, True)
result = dict(
changed=False,
msg='The certificate is still valid and no condition was reached',
msg="The certificate is still valid and no condition was reached",
exists=False,
parsable=False,
supports_ari=False,
)
def complete(should_renew, **kwargs):
result['should_renew'] = should_renew
result["should_renew"] = should_renew
result.update(kwargs)
module.exit_json(**result)
if not module.params['certificate_path'] and not module.params['certificate_content']:
complete(True, msg='No certificate was specified')
if (
not module.params["certificate_path"]
and not module.params["certificate_content"]
):
complete(True, msg="No certificate was specified")
if module.params['certificate_path'] is not None:
if not os.path.exists(module.params['certificate_path']):
complete(True, msg='The certificate file does not exist')
if module.params['treat_parsing_error_as_non_existing']:
if module.params["certificate_path"] is not None:
if not os.path.exists(module.params["certificate_path"]):
complete(True, msg="The certificate file does not exist")
if module.params["treat_parsing_error_as_non_existing"]:
try:
read_file(module.params['certificate_path'])
read_file(module.params["certificate_path"])
except ModuleFailException as e:
e.do_fail(module)
result['exists'] = True
result["exists"] = True
try:
cert_info = backend.get_cert_information(
cert_filename=module.params['certificate_path'],
cert_content=module.params['certificate_content'],
cert_filename=module.params["certificate_path"],
cert_content=module.params["certificate_content"],
)
except ModuleFailException as e:
if module.params['treat_parsing_error_as_non_existing']:
complete(True, msg='Certificate cannot be parsed: {0}'.format(e.msg))
if module.params["treat_parsing_error_as_non_existing"]:
complete(True, msg="Certificate cannot be parsed: {0}".format(e.msg))
e.do_fail(module)
result['parsable'] = True
result["parsable"] = True
try:
cert_id = compute_cert_id(backend, cert_info=cert_info, none_if_required_information_is_missing=True)
cert_id = compute_cert_id(
backend, cert_info=cert_info, none_if_required_information_is_missing=True
)
if cert_id is not None:
result['cert_id'] = cert_id
result["cert_id"] = cert_id
if module.params['now']:
now = backend.parse_module_parameter(module.params['now'], 'now')
if module.params["now"]:
now = backend.parse_module_parameter(module.params["now"], "now")
else:
now = backend.get_now()
if now >= cert_info.not_valid_after:
complete(True, msg='The certificate has already expired')
complete(True, msg="The certificate has already expired")
client = ACMEClient(module, backend)
if cert_id is not None and module.params['use_ari'] and client.directory.has_renewal_info_endpoint():
if (
cert_id is not None
and module.params["use_ari"]
and client.directory.has_renewal_info_endpoint()
):
renewal_info = client.get_renewal_info(cert_id=cert_id)
window_start = backend.parse_acme_timestamp(renewal_info['suggestedWindow']['start'])
window_end = backend.parse_acme_timestamp(renewal_info['suggestedWindow']['end'])
msg_append = ''
if 'explanationURL' in renewal_info:
msg_append = '. Information on renewal interval: {0}'.format(renewal_info['explanationURL'])
result['supports_ari'] = True
window_start = backend.parse_acme_timestamp(
renewal_info["suggestedWindow"]["start"]
)
window_end = backend.parse_acme_timestamp(
renewal_info["suggestedWindow"]["end"]
)
msg_append = ""
if "explanationURL" in renewal_info:
msg_append = ". Information on renewal interval: {0}".format(
renewal_info["explanationURL"]
)
result["supports_ari"] = True
if now > window_end:
complete(True, msg='The suggested renewal interval provided by ARI is in the past{0}'.format(msg_append))
if module.params['ari_algorithm'] == 'start':
complete(
True,
msg="The suggested renewal interval provided by ARI is in the past{0}".format(
msg_append
),
)
if module.params["ari_algorithm"] == "start":
if now > window_start:
complete(True, msg='The suggested renewal interval provided by ARI has begun{0}'.format(msg_append))
complete(
True,
msg="The suggested renewal interval provided by ARI has begun{0}".format(
msg_append
),
)
else:
random_time = backend.interpolate_timestamp(window_start, window_end, random.random())
random_time = backend.interpolate_timestamp(
window_start, window_end, random.random()
)
if now > random_time:
complete(
True,
msg='The picked random renewal time {0} in sugested renewal internal provided by ARI is in the past{1}'.format(
msg="The picked random renewal time {0} in sugested renewal internal provided by ARI is in the past{1}".format(
random_time,
msg_append,
),
)
if module.params['remaining_days'] is not None:
if module.params["remaining_days"] is not None:
remaining_days = (cert_info.not_valid_after - now).days
if remaining_days < module.params['remaining_days']:
complete(True, msg='The certificate expires in {0} days'.format(remaining_days))
if remaining_days < module.params["remaining_days"]:
complete(
True,
msg="The certificate expires in {0} days".format(remaining_days),
)
if module.params['remaining_percentage'] is not None:
timestamp = backend.interpolate_timestamp(cert_info.not_valid_before, cert_info.not_valid_after, 1 - module.params['remaining_percentage'])
if module.params["remaining_percentage"] is not None:
timestamp = backend.interpolate_timestamp(
cert_info.not_valid_before,
cert_info.not_valid_after,
1 - module.params["remaining_percentage"],
)
if timestamp < now:
complete(
True,
msg="The remaining percentage {0}% of the certificate's lifespan was reached on {1}".format(
module.params['remaining_percentage'] * 100,
module.params["remaining_percentage"] * 100,
timestamp,
),
)
@@ -296,5 +330,5 @@ def main():
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -137,18 +137,28 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.utils import
def main():
argument_spec = create_default_argspec(require_account_key=False)
argument_spec.update_argspec(
private_key_src=dict(type='path'),
private_key_content=dict(type='str', no_log=True),
private_key_passphrase=dict(type='str', no_log=True),
certificate=dict(type='path', required=True),
revoke_reason=dict(type='int'),
private_key_src=dict(type="path"),
private_key_content=dict(type="str", no_log=True),
private_key_passphrase=dict(type="str", no_log=True),
certificate=dict(type="path", required=True),
revoke_reason=dict(type="int"),
)
argument_spec.update(
required_one_of=(
['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
[
"account_key_src",
"account_key_content",
"private_key_src",
"private_key_content",
],
),
mutually_exclusive=(
['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
[
"account_key_src",
"account_key_content",
"private_key_src",
"private_key_content",
],
),
)
module = argument_spec.create_ansible_module()
@@ -158,70 +168,86 @@ def main():
client = ACMEClient(module, backend)
account = ACMEAccount(client)
# Load certificate
certificate = pem_to_der(module.params.get('certificate'))
certificate = pem_to_der(module.params.get("certificate"))
certificate = nopad_b64(certificate)
# Construct payload
payload = {
'certificate': certificate
}
if module.params.get('revoke_reason') is not None:
payload['reason'] = module.params.get('revoke_reason')
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'
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')
private_key = module.params.get("private_key_src")
private_key_content = module.params.get("private_key_content")
# Revoke certificate
if private_key or private_key_content:
passphrase = module.params['private_key_passphrase']
passphrase = module.params["private_key_passphrase"]
# Step 1: load and parse private key
try:
private_key_data = client.parse_key(private_key, private_key_content, passphrase=passphrase)
private_key_data = client.parse_key(
private_key, private_key_content, passphrase=passphrase
)
except KeyParsingError as e:
raise ModuleFailException("Error while parsing private key: {msg}".format(msg=e.msg))
raise ModuleFailException(
"Error while parsing private key: {msg}".format(msg=e.msg)
)
# Step 2: sign revokation request with private key
jws_header = {
"alg": private_key_data['alg'],
"jwk": private_key_data['jwk'],
"alg": private_key_data["alg"],
"jwk": private_key_data["jwk"],
}
result, info = client.send_signed_request(
endpoint, payload, key_data=private_key_data, jws_header=jws_header, fail_on_error=False)
endpoint,
payload,
key_data=private_key_data,
jws_header=jws_header,
fail_on_error=False,
)
else:
# Step 1: get hold of account URI
created, account_data = account.setup_account(allow_creation=False)
if created:
raise AssertionError('Unwanted account creation')
raise AssertionError("Unwanted account creation")
if account_data is None:
raise ModuleFailException(msg='Account does not exist or is deactivated.')
raise ModuleFailException(
msg="Account does not exist or is deactivated."
)
# Step 2: sign revokation request with account key
result, info = client.send_signed_request(endpoint, payload, fail_on_error=False)
if info['status'] != 200:
result, info = client.send_signed_request(
endpoint, payload, fail_on_error=False
)
if info["status"] != 200:
already_revoked = False
# Standardized error from draft 14 on (https://tools.ietf.org/html/rfc8555#section-7.6)
if result.get('type') == 'urn:ietf:params:acme:error:alreadyRevoked':
if result.get("type") == "urn:ietf:params:acme:error:alreadyRevoked":
already_revoked = True
else:
# Hack for Boulder errors
if module.params.get('acme_version') == 1:
error_type = 'urn:acme:error:malformed'
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 and result.get('detail') == 'Certificate already revoked':
error_type = "urn:ietf:params:acme:error:malformed"
if (
result.get("type") == error_type
and result.get("detail") == "Certificate already revoked"
):
# Fallback: boulder returns this in case the certificate was already revoked.
already_revoked = True
# If we know the certificate was already revoked, we do not fail,
# but successfully terminate while indicating no change
if already_revoked:
module.exit_json(changed=False)
raise ACMEProtocolException(module, 'Failed to revoke certificate', info=info, content_json=result)
raise ACMEProtocolException(
module, "Failed to revoke certificate", info=info, content_json=result
)
module.exit_json(changed=True)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -190,7 +190,8 @@ try:
import cryptography.hazmat.primitives.serialization
import cryptography.x509
import cryptography.x509.oid
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.3'))
HAS_CRYPTOGRAPHY = LooseVersion(cryptography.__version__) >= LooseVersion("1.3")
_cryptography_backend = cryptography.hazmat.backends.default_backend()
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -199,121 +200,137 @@ except ImportError:
# Convert byte string to ASN1 encoded octet string
if sys.version_info[0] >= 3:
def encode_octet_string(octet_string):
if len(octet_string) >= 128:
raise ModuleFailException('Cannot handle octet strings with more than 128 bytes')
raise ModuleFailException(
"Cannot handle octet strings with more than 128 bytes"
)
return bytes([0x4, len(octet_string)]) + octet_string
else:
def encode_octet_string(octet_string):
if len(octet_string) >= 128:
raise ModuleFailException('Cannot handle octet strings with more than 128 bytes')
return b'\x04' + chr(len(octet_string)) + octet_string
raise ModuleFailException(
"Cannot handle octet strings with more than 128 bytes"
)
return b"\x04" + chr(len(octet_string)) + octet_string
def main():
module = AnsibleModule(
argument_spec=dict(
challenge=dict(type='str', required=True, choices=['tls-alpn-01']),
challenge_data=dict(type='dict', required=True),
private_key_src=dict(type='path'),
private_key_content=dict(type='str', no_log=True),
private_key_passphrase=dict(type='str', no_log=True),
),
required_one_of=(
['private_key_src', 'private_key_content'],
),
mutually_exclusive=(
['private_key_src', 'private_key_content'],
challenge=dict(type="str", required=True, choices=["tls-alpn-01"]),
challenge_data=dict(type="dict", required=True),
private_key_src=dict(type="path"),
private_key_content=dict(type="str", no_log=True),
private_key_passphrase=dict(type="str", no_log=True),
),
required_one_of=(["private_key_src", "private_key_content"],),
mutually_exclusive=(["private_key_src", "private_key_content"],),
)
if not HAS_CRYPTOGRAPHY:
# Some callbacks die when exception is provided with value None
if CRYPTOGRAPHY_IMP_ERR:
module.fail_json(msg=missing_required_lib('cryptography >= 1.3'), exception=CRYPTOGRAPHY_IMP_ERR)
module.fail_json(msg=missing_required_lib('cryptography >= 1.3'))
module.fail_json(
msg=missing_required_lib("cryptography >= 1.3"),
exception=CRYPTOGRAPHY_IMP_ERR,
)
module.fail_json(msg=missing_required_lib("cryptography >= 1.3"))
try:
# Get parameters
challenge = module.params['challenge']
challenge_data = module.params['challenge_data']
challenge = module.params["challenge"]
challenge_data = module.params["challenge_data"]
# Get hold of private key
private_key_content = module.params.get('private_key_content')
private_key_passphrase = module.params.get('private_key_passphrase')
private_key_content = module.params.get("private_key_content")
private_key_passphrase = module.params.get("private_key_passphrase")
if private_key_content is None:
private_key_content = read_file(module.params['private_key_src'])
private_key_content = read_file(module.params["private_key_src"])
else:
private_key_content = to_bytes(private_key_content)
try:
private_key = cryptography.hazmat.primitives.serialization.load_pem_private_key(
private_key_content,
password=to_bytes(private_key_passphrase) if private_key_passphrase is not None else None,
backend=_cryptography_backend)
private_key = (
cryptography.hazmat.primitives.serialization.load_pem_private_key(
private_key_content,
password=(
to_bytes(private_key_passphrase)
if private_key_passphrase is not None
else None
),
backend=_cryptography_backend,
)
)
except Exception as e:
raise ModuleFailException('Error while loading private key: {0}'.format(e))
raise ModuleFailException("Error while loading private key: {0}".format(e))
# Some common attributes
domain = to_text(challenge_data['resource'])
identifier_type, identifier = to_text(challenge_data.get('resource_original', 'dns:' + challenge_data['resource'])).split(':', 1)
domain = to_text(challenge_data["resource"])
identifier_type, identifier = to_text(
challenge_data.get("resource_original", "dns:" + challenge_data["resource"])
).split(":", 1)
subject = issuer = cryptography.x509.Name([])
now = get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE)
not_valid_before = now
not_valid_after = now + datetime.timedelta(days=10)
if identifier_type == 'dns':
if identifier_type == "dns":
san = cryptography.x509.DNSName(identifier)
elif identifier_type == 'ip':
elif identifier_type == "ip":
san = cryptography.x509.IPAddress(ipaddress.ip_address(identifier))
else:
raise ModuleFailException('Unsupported identifier type "{0}"'.format(identifier_type))
raise ModuleFailException(
'Unsupported identifier type "{0}"'.format(identifier_type)
)
# Generate regular self-signed certificate
cert_builder = cryptography.x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
private_key.public_key()
).serial_number(
cryptography.x509.random_serial_number()
).add_extension(
cryptography.x509.SubjectAlternativeName([san]),
critical=False,
cert_builder = (
cryptography.x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(private_key.public_key())
.serial_number(cryptography.x509.random_serial_number())
.add_extension(
cryptography.x509.SubjectAlternativeName([san]),
critical=False,
)
)
cert_builder = set_not_valid_before(cert_builder, not_valid_before)
cert_builder = set_not_valid_after(cert_builder, not_valid_after)
regular_certificate = cert_builder.sign(
private_key,
cryptography.hazmat.primitives.hashes.SHA256(),
_cryptography_backend
_cryptography_backend,
)
# Process challenge
if challenge == 'tls-alpn-01':
value = base64.b64decode(challenge_data['resource_value'])
cert_builder = cryptography.x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
private_key.public_key()
).serial_number(
cryptography.x509.random_serial_number()
).add_extension(
cryptography.x509.SubjectAlternativeName([san]),
critical=False,
).add_extension(
cryptography.x509.UnrecognizedExtension(
cryptography.x509.ObjectIdentifier("1.3.6.1.5.5.7.1.31"),
encode_octet_string(value),
),
critical=True,
if challenge == "tls-alpn-01":
value = base64.b64decode(challenge_data["resource_value"])
cert_builder = (
cryptography.x509.CertificateBuilder()
.subject_name(subject)
.issuer_name(issuer)
.public_key(private_key.public_key())
.serial_number(cryptography.x509.random_serial_number())
.add_extension(
cryptography.x509.SubjectAlternativeName([san]),
critical=False,
)
.add_extension(
cryptography.x509.UnrecognizedExtension(
cryptography.x509.ObjectIdentifier("1.3.6.1.5.5.7.1.31"),
encode_octet_string(value),
),
critical=True,
)
)
cert_builder = set_not_valid_before(cert_builder, not_valid_before)
cert_builder = set_not_valid_after(cert_builder, not_valid_after)
challenge_certificate = cert_builder.sign(
private_key,
cryptography.hazmat.primitives.hashes.SHA256(),
_cryptography_backend
_cryptography_backend,
)
module.exit_json(
@@ -321,12 +338,16 @@ def main():
domain=domain,
identifier_type=identifier_type,
identifier=identifier,
challenge_certificate=challenge_certificate.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM),
regular_certificate=regular_certificate.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.PEM)
challenge_certificate=challenge_certificate.public_bytes(
cryptography.hazmat.primitives.serialization.Encoding.PEM
),
regular_certificate=regular_certificate.public_bytes(
cryptography.hazmat.primitives.serialization.Encoding.PEM
),
)
except ModuleFailException as e:
e.do_fail(module)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -252,17 +252,19 @@ from ansible_collections.community.crypto.plugins.module_utils.acme.errors impor
def main():
argument_spec = create_default_argspec(require_account_key=False)
argument_spec.update_argspec(
url=dict(type='str'),
method=dict(type='str', choices=['get', 'post', 'directory-only'], default='get'),
content=dict(type='str'),
fail_on_acme_error=dict(type='bool', default=True),
url=dict(type="str"),
method=dict(
type="str", choices=["get", "post", "directory-only"], default="get"
),
content=dict(type="str"),
fail_on_acme_error=dict(type="bool", default=True),
)
argument_spec.update(
required_if=(
['method', 'get', ['url']],
['method', 'post', ['url', 'content']],
['method', 'get', ['account_key_src', 'account_key_content'], True],
['method', 'post', ['account_key_src', 'account_key_content'], True],
["method", "get", ["url"]],
["method", "post", ["url", "content"]],
["method", "get", ["account_key_src", "account_key_content"], True],
["method", "post", ["account_key_src", "account_key_content"], True],
),
)
module = argument_spec.create_ansible_module()
@@ -273,31 +275,40 @@ def main():
try:
# Get hold of ACMEClient and ACMEAccount objects (includes directory)
client = ACMEClient(module, backend)
method = module.params['method']
result['directory'] = client.directory.directory
method = module.params["method"]
result["directory"] = client.directory.directory
# Do we have to do more requests?
if method != 'directory-only':
url = module.params['url']
fail_on_acme_error = module.params['fail_on_acme_error']
if method != "directory-only":
url = module.params["url"]
fail_on_acme_error = module.params["fail_on_acme_error"]
# Do request
if method == 'get':
data, info = client.get_request(url, parse_json_result=False, fail_on_error=False)
elif method == 'post':
if method == "get":
data, info = client.get_request(
url, parse_json_result=False, fail_on_error=False
)
elif method == "post":
changed = True # only POSTs can change
data, info = client.send_signed_request(
url, to_bytes(module.params['content']), parse_json_result=False, encode_payload=False, fail_on_error=False)
url,
to_bytes(module.params["content"]),
parse_json_result=False,
encode_payload=False,
fail_on_error=False,
)
# Update results
result.update(dict(
headers=info,
output_text=to_native(data),
))
result.update(
dict(
headers=info,
output_text=to_native(data),
)
)
# See if we can parse the result as JSON
try:
result['output_json'] = module.from_json(to_text(data))
result["output_json"] = module.from_json(to_text(data))
except Exception:
pass
# Fail if error was returned
if fail_on_acme_error and info['status'] >= 400:
if fail_on_acme_error and info["status"] >= 400:
raise ACMEProtocolException(module, info=info, content=data)
# Done!
module.exit_json(changed=changed, **result)
@@ -305,5 +316,5 @@ def main():
e.do_fail(module, **result)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -156,7 +156,8 @@ try:
import cryptography.hazmat.primitives.serialization
import cryptography.x509
import cryptography.x509.oid
HAS_CRYPTOGRAPHY = (LooseVersion(cryptography.__version__) >= LooseVersion('1.5'))
HAS_CRYPTOGRAPHY = LooseVersion(cryptography.__version__) >= LooseVersion("1.5")
_cryptography_backend = cryptography.hazmat.backends.default_backend()
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -164,44 +165,55 @@ except ImportError:
class Certificate(object):
'''
"""
Stores PEM with parsed certificate.
'''
"""
def __init__(self, pem, cert):
if not (pem.endswith('\n') or pem.endswith('\r')):
pem = pem + '\n'
if not (pem.endswith("\n") or pem.endswith("\r")):
pem = pem + "\n"
self.pem = pem
self.cert = cert
def is_parent(module, cert, potential_parent):
'''
"""
Tests whether the given certificate has been issued by the potential parent certificate.
'''
"""
# Check issuer
if cert.cert.issuer != potential_parent.cert.subject:
return False
# Check signature
public_key = potential_parent.cert.public_key()
try:
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
if isinstance(
public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey
):
public_key.verify(
cert.cert.signature,
cert.cert.tbs_certificate_bytes,
cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15(),
cert.cert.signature_hash_algorithm
cert.cert.signature_hash_algorithm,
)
elif isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
elif isinstance(
public_key,
cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey,
):
public_key.verify(
cert.cert.signature,
cert.cert.tbs_certificate_bytes,
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(cert.cert.signature_hash_algorithm),
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(
cert.cert.signature_hash_algorithm
),
)
elif CRYPTOGRAPHY_HAS_ED25519_SIGN and isinstance(
public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
public_key,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey,
):
public_key.verify(cert.cert.signature, cert.cert.tbs_certificate_bytes)
elif CRYPTOGRAPHY_HAS_ED448_SIGN and isinstance(
public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey
):
public_key.verify(cert.cert.signature, cert.cert.tbs_certificate_bytes)
else:
# Unknown public key type
@@ -211,24 +223,30 @@ def is_parent(module, cert, potential_parent):
except cryptography.exceptions.InvalidSignature:
return False
except cryptography.exceptions.UnsupportedAlgorithm:
module.warn('Unsupported algorithm "{0}"'.format(cert.cert.signature_hash_algorithm))
module.warn(
'Unsupported algorithm "{0}"'.format(cert.cert.signature_hash_algorithm)
)
return False
except Exception as e:
module.fail_json(msg='Unknown error on signature validation: {0}'.format(e))
module.fail_json(msg="Unknown error on signature validation: {0}".format(e))
def parse_PEM_list(module, text, source, fail_on_error=True):
'''
"""
Parse concatenated PEM certificates. Return list of ``Certificate`` objects.
'''
"""
result = []
for cert_pem in split_pem_list(text):
# Try to load PEM certificate
try:
cert = cryptography.x509.load_pem_x509_certificate(to_bytes(cert_pem), _cryptography_backend)
cert = cryptography.x509.load_pem_x509_certificate(
to_bytes(cert_pem), _cryptography_backend
)
result.append(Certificate(cert_pem, cert))
except Exception as e:
msg = 'Cannot parse certificate #{0} from {1}: {2}'.format(len(result) + 1, source, e)
msg = "Cannot parse certificate #{0} from {1}: {2}".format(
len(result) + 1, source, e
)
if fail_on_error:
module.fail_json(msg=msg)
else:
@@ -237,14 +255,19 @@ def parse_PEM_list(module, text, source, fail_on_error=True):
def load_PEM_list(module, path, fail_on_error=True):
'''
"""
Load concatenated PEM certificates from file. Return list of ``Certificate`` objects.
'''
"""
try:
with open(path, "rb") as f:
return parse_PEM_list(module, f.read().decode('utf-8'), source=path, fail_on_error=fail_on_error)
return parse_PEM_list(
module,
f.read().decode("utf-8"),
source=path,
fail_on_error=fail_on_error,
)
except Exception as e:
msg = 'Cannot read certificate file {0}: {1}'.format(path, e)
msg = "Cannot read certificate file {0}: {1}".format(path, e)
if fail_on_error:
module.fail_json(msg=msg)
else:
@@ -253,9 +276,9 @@ def load_PEM_list(module, path, fail_on_error=True):
class CertificateSet(object):
'''
"""
Stores a set of certificates. Allows to search for parent (issuer of a certificate).
'''
"""
def __init__(self, module):
self.module = module
@@ -273,10 +296,10 @@ class CertificateSet(object):
self.certificate_by_cert[cert.cert] = cert
def load(self, path):
'''
"""
Load lists of PEM certificates from a file or a directory.
'''
b_path = to_bytes(path, errors='surrogate_or_strict')
"""
b_path = to_bytes(path, errors="surrogate_or_strict")
if os.path.isdir(b_path):
for directory, dummy, files in os.walk(b_path, followlinks=True):
for file in files:
@@ -285,9 +308,9 @@ class CertificateSet(object):
self._load_file(b_path)
def find_parent(self, cert):
'''
"""
Search for the parent (issuer) of a certificate. Return ``None`` if none was found.
'''
"""
potential_parents = self.certificates_by_issuer.get(cert.cert.issuer, [])
for potential_parent in potential_parents:
if is_parent(self.module, cert, potential_parent):
@@ -296,55 +319,62 @@ class CertificateSet(object):
def format_cert(cert):
'''
"""
Return human readable representation of certificate for error messages.
'''
"""
return str(cert.cert)
def check_cycle(module, occured_certificates, next):
'''
"""
Make sure that next is not in occured_certificates so far, and add it.
'''
"""
next_cert = next.cert
if next_cert in occured_certificates:
module.fail_json(msg='Found cycle while building certificate chain')
module.fail_json(msg="Found cycle while building certificate chain")
occured_certificates.add(next_cert)
def main():
module = AnsibleModule(
argument_spec=dict(
input_chain=dict(type='str', required=True),
root_certificates=dict(type='list', required=True, elements='path'),
intermediate_certificates=dict(type='list', default=[], elements='path'),
input_chain=dict(type="str", required=True),
root_certificates=dict(type="list", required=True, elements="path"),
intermediate_certificates=dict(type="list", default=[], elements="path"),
),
supports_check_mode=True,
)
if not HAS_CRYPTOGRAPHY:
module.fail_json(msg=missing_required_lib('cryptography >= 1.5'), exception=CRYPTOGRAPHY_IMP_ERR)
module.fail_json(
msg=missing_required_lib("cryptography >= 1.5"),
exception=CRYPTOGRAPHY_IMP_ERR,
)
# Load chain
chain = parse_PEM_list(module, module.params['input_chain'], source='input chain')
chain = parse_PEM_list(module, module.params["input_chain"], source="input chain")
if len(chain) == 0:
module.fail_json(msg='Input chain must contain at least one certificate')
module.fail_json(msg="Input chain must contain at least one certificate")
# Check chain
for i, parent in enumerate(chain):
if i > 0:
if not is_parent(module, chain[i - 1], parent):
module.fail_json(msg=('Cannot verify input chain: certificate #{2}: {3} is not issuer ' +
'of certificate #{0}: {1}').format(i, format_cert(chain[i - 1]), i + 1, format_cert(parent)))
module.fail_json(
msg=(
"Cannot verify input chain: certificate #{2}: {3} is not issuer "
+ "of certificate #{0}: {1}"
).format(i, format_cert(chain[i - 1]), i + 1, format_cert(parent))
)
# Load intermediate certificates
intermediates = CertificateSet(module)
for path in module.params['intermediate_certificates']:
for path in module.params["intermediate_certificates"]:
intermediates.load(path)
# Load root certificates
roots = CertificateSet(module)
for path in module.params['root_certificates']:
for path in module.params["root_certificates"]:
roots.load(path)
# Try to complete chain
@@ -366,7 +396,11 @@ def main():
completed.append(intermediate)
current = intermediate
else:
module.fail_json(msg='Cannot complete chain. Stuck at certificate {0}'.format(format_cert(current)))
module.fail_json(
msg="Cannot complete chain. Stuck at certificate {0}".format(
format_cert(current)
)
)
# Return results
complete_chain = chain + completed

View File

@@ -198,33 +198,33 @@ else:
CURVES = (
('secp224r1', 'SECP224R1'),
('secp256k1', 'SECP256K1'),
('secp256r1', 'SECP256R1'),
('secp384r1', 'SECP384R1'),
('secp521r1', 'SECP521R1'),
('secp192r1', 'SECP192R1'),
('sect163k1', 'SECT163K1'),
('sect163r2', 'SECT163R2'),
('sect233k1', 'SECT233K1'),
('sect233r1', 'SECT233R1'),
('sect283k1', 'SECT283K1'),
('sect283r1', 'SECT283R1'),
('sect409k1', 'SECT409K1'),
('sect409r1', 'SECT409R1'),
('sect571k1', 'SECT571K1'),
('sect571r1', 'SECT571R1'),
('brainpoolP256r1', 'BrainpoolP256R1'),
('brainpoolP384r1', 'BrainpoolP384R1'),
('brainpoolP512r1', 'BrainpoolP512R1'),
("secp224r1", "SECP224R1"),
("secp256k1", "SECP256K1"),
("secp256r1", "SECP256R1"),
("secp384r1", "SECP384R1"),
("secp521r1", "SECP521R1"),
("secp192r1", "SECP192R1"),
("sect163k1", "SECT163K1"),
("sect163r2", "SECT163R2"),
("sect233k1", "SECT233K1"),
("sect233r1", "SECT233R1"),
("sect283k1", "SECT283K1"),
("sect283r1", "SECT283R1"),
("sect409k1", "SECT409K1"),
("sect409r1", "SECT409R1"),
("sect571k1", "SECT571K1"),
("sect571r1", "SECT571R1"),
("brainpoolP256r1", "BrainpoolP256R1"),
("brainpoolP384r1", "BrainpoolP384R1"),
("brainpoolP512r1", "BrainpoolP512R1"),
)
def add_crypto_information(module):
result = {}
result['python_cryptography_installed'] = HAS_CRYPTOGRAPHY
result["python_cryptography_installed"] = HAS_CRYPTOGRAPHY
if not HAS_CRYPTOGRAPHY:
result['python_cryptography_import_error'] = CRYPTOGRAPHY_IMP_ERR
result["python_cryptography_import_error"] = CRYPTOGRAPHY_IMP_ERR
return result
has_ed25519 = CRYPTOGRAPHY_HAS_ED25519
@@ -233,7 +233,8 @@ def add_crypto_information(module):
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PrivateKey,
)
Ed25519PrivateKey.from_private_bytes(b'')
Ed25519PrivateKey.from_private_bytes(b"")
except ValueError:
pass
except UnsupportedAlgorithm:
@@ -243,7 +244,8 @@ def add_crypto_information(module):
if has_ed448:
try:
from cryptography.hazmat.primitives.asymmetric.ed448 import Ed448PrivateKey
Ed448PrivateKey.from_private_bytes(b'')
Ed448PrivateKey.from_private_bytes(b"")
except ValueError:
pass
except UnsupportedAlgorithm:
@@ -255,8 +257,9 @@ def add_crypto_information(module):
from cryptography.hazmat.primitives.asymmetric.x25519 import (
X25519PrivateKey,
)
if CRYPTOGRAPHY_HAS_X25519_FULL:
X25519PrivateKey.from_private_bytes(b'')
X25519PrivateKey.from_private_bytes(b"")
else:
# Some versions do not support serialization and deserialization - use generate() instead
X25519PrivateKey.generate()
@@ -269,7 +272,8 @@ def add_crypto_information(module):
if has_x448:
try:
from cryptography.hazmat.primitives.asymmetric.x448 import X448PrivateKey
X448PrivateKey.from_private_bytes(b'')
X448PrivateKey.from_private_bytes(b"")
except ValueError:
pass
except UnsupportedAlgorithm:
@@ -282,59 +286,65 @@ def add_crypto_information(module):
backend = cryptography.hazmat.backends.default_backend()
for curve_name, constructor_name in CURVES:
ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(constructor_name)
ecclass = cryptography.hazmat.primitives.asymmetric.ec.__dict__.get(
constructor_name
)
if ecclass:
try:
cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(curve=ecclass(), backend=backend)
cryptography.hazmat.primitives.asymmetric.ec.generate_private_key(
curve=ecclass(), backend=backend
)
curves.append(curve_name)
except UnsupportedAlgorithm:
pass
except CryptographyInternalError: # pylint: disable=duplicate-except,bad-except-order
except ( # pylint: disable=duplicate-except,bad-except-order
CryptographyInternalError
):
# On Fedora 41, some curves result in InternalError. This is probably because
# Fedora's cryptography is linked against the system libssl, which has the
# curves removed.
pass
info = {
'version': CRYPTOGRAPHY_VERSION,
'curves': curves,
'has_ec': CRYPTOGRAPHY_HAS_EC,
'has_ec_sign': CRYPTOGRAPHY_HAS_EC_SIGN,
'has_ed25519': has_ed25519,
'has_ed25519_sign': has_ed25519 and CRYPTOGRAPHY_HAS_ED25519_SIGN,
'has_ed448': has_ed448,
'has_ed448_sign': has_ed448 and CRYPTOGRAPHY_HAS_ED448_SIGN,
'has_dsa': CRYPTOGRAPHY_HAS_DSA,
'has_dsa_sign': CRYPTOGRAPHY_HAS_DSA_SIGN,
'has_rsa': CRYPTOGRAPHY_HAS_RSA,
'has_rsa_sign': CRYPTOGRAPHY_HAS_RSA_SIGN,
'has_x25519': has_x25519,
'has_x25519_serialization': has_x25519 and CRYPTOGRAPHY_HAS_X25519_FULL,
'has_x448': has_x448,
"version": CRYPTOGRAPHY_VERSION,
"curves": curves,
"has_ec": CRYPTOGRAPHY_HAS_EC,
"has_ec_sign": CRYPTOGRAPHY_HAS_EC_SIGN,
"has_ed25519": has_ed25519,
"has_ed25519_sign": has_ed25519 and CRYPTOGRAPHY_HAS_ED25519_SIGN,
"has_ed448": has_ed448,
"has_ed448_sign": has_ed448 and CRYPTOGRAPHY_HAS_ED448_SIGN,
"has_dsa": CRYPTOGRAPHY_HAS_DSA,
"has_dsa_sign": CRYPTOGRAPHY_HAS_DSA_SIGN,
"has_rsa": CRYPTOGRAPHY_HAS_RSA,
"has_rsa_sign": CRYPTOGRAPHY_HAS_RSA_SIGN,
"has_x25519": has_x25519,
"has_x25519_serialization": has_x25519 and CRYPTOGRAPHY_HAS_X25519_FULL,
"has_x448": has_x448,
}
result['python_cryptography_capabilities'] = info
result["python_cryptography_capabilities"] = info
return result
def add_openssl_information(module):
openssl_binary = module.get_bin_path('openssl')
openssl_binary = module.get_bin_path("openssl")
result = {
'openssl_present': openssl_binary is not None,
"openssl_present": openssl_binary is not None,
}
if openssl_binary is None:
return result
openssl_result = {
'path': openssl_binary,
"path": openssl_binary,
}
result['openssl'] = openssl_result
result["openssl"] = openssl_result
rc, out, err = module.run_command([openssl_binary, 'version'])
rc, out, err = module.run_command([openssl_binary, "version"])
if rc == 0:
openssl_result['version_output'] = out
openssl_result["version_output"] = out
parts = out.split(None, 2)
if len(parts) > 1:
openssl_result['version'] = parts[1]
openssl_result["version"] = parts[1]
return result
@@ -353,5 +363,5 @@ def main():
module.exit_json(**result)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -578,6 +578,7 @@ from ansible_collections.community.crypto.plugins.module_utils.version import (
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -585,14 +586,20 @@ except ImportError:
else:
CRYPTOGRAPHY_FOUND = True
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
MINIMAL_CRYPTOGRAPHY_VERSION = "1.6"
def validate_cert_expiry(cert_expiry):
search_string_partial = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])\Z')
search_string_full = re.compile(r'^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):'
r'([0-5][0-9]|60)(.[0-9]+)?(([Zz])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))\Z')
if search_string_partial.match(cert_expiry) or search_string_full.match(cert_expiry):
search_string_partial = re.compile(
r"^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])\Z"
)
search_string_full = re.compile(
r"^([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):"
r"([0-5][0-9]|60)(.[0-9]+)?(([Zz])|([+|-]([01][0-9]|2[0-3]):[0-5][0-9]))\Z"
)
if search_string_partial.match(cert_expiry) or search_string_full.match(
cert_expiry
):
return True
return False
@@ -600,7 +607,9 @@ def validate_cert_expiry(cert_expiry):
def calculate_cert_days(expires_after):
cert_days = 0
if expires_after:
expires_after_datetime = datetime.datetime.strptime(expires_after, '%Y-%m-%dT%H:%M:%SZ')
expires_after_datetime = datetime.datetime.strptime(
expires_after, "%Y-%m-%dT%H:%M:%SZ"
)
cert_days = (expires_after_datetime - datetime.datetime.now()).days
return cert_days
@@ -612,24 +621,24 @@ def convert_module_param_to_json_bool(module, dict_param_name, param_name):
body = {}
if module.params[param_name] is not None:
if module.params[param_name]:
body[dict_param_name] = 'true'
body[dict_param_name] = "true"
else:
body[dict_param_name] = 'false'
body[dict_param_name] = "false"
return body
class EcsCertificate(object):
'''
"""
Entrust Certificate Services certificate class.
'''
"""
def __init__(self, module):
self.path = module.params['path']
self.full_chain_path = module.params['full_chain_path']
self.force = module.params['force']
self.backup = module.params['backup']
self.request_type = module.params['request_type']
self.csr = module.params['csr']
self.path = module.params["path"]
self.full_chain_path = module.params["full_chain_path"]
self.force = module.params["force"]
self.backup = module.params["backup"]
self.request_type = module.params["request_type"]
self.csr = module.params["csr"]
# All return values
self.changed = False
@@ -646,82 +655,94 @@ class EcsCertificate(object):
self.ecs_client = None
if self.path and os.path.exists(self.path):
try:
self.cert = load_certificate(self.path, backend='cryptography')
self.cert = load_certificate(self.path, backend="cryptography")
except Exception:
self.cert = None
# Instantiate the ECS client and then try a no-op connection to verify credentials are valid
try:
self.ecs_client = ECSClient(
entrust_api_user=module.params['entrust_api_user'],
entrust_api_key=module.params['entrust_api_key'],
entrust_api_cert=module.params['entrust_api_client_cert_path'],
entrust_api_cert_key=module.params['entrust_api_client_cert_key_path'],
entrust_api_specification_path=module.params['entrust_api_specification_path']
entrust_api_user=module.params["entrust_api_user"],
entrust_api_key=module.params["entrust_api_key"],
entrust_api_cert=module.params["entrust_api_client_cert_path"],
entrust_api_cert_key=module.params["entrust_api_client_cert_key_path"],
entrust_api_specification_path=module.params[
"entrust_api_specification_path"
],
)
except SessionConfigurationException as e:
module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e)))
module.fail_json(
msg="Failed to initialize Entrust Provider: {0}".format(to_native(e))
)
try:
self.ecs_client.GetAppVersion()
except RestOperationException as e:
module.fail_json(msg='Please verify credential information. Received exception when testing ECS connection: {0}'.format(to_native(e.message)))
module.fail_json(
msg="Please verify credential information. Received exception when testing ECS connection: {0}".format(
to_native(e.message)
)
)
# Conversion of the fields that go into the 'tracking' parameter of the request object
def convert_tracking_params(self, module):
body = {}
tracking = {}
if module.params['requester_name']:
tracking['requesterName'] = module.params['requester_name']
if module.params['requester_email']:
tracking['requesterEmail'] = module.params['requester_email']
if module.params['requester_phone']:
tracking['requesterPhone'] = module.params['requester_phone']
if module.params['tracking_info']:
tracking['trackingInfo'] = module.params['tracking_info']
if module.params['custom_fields']:
if module.params["requester_name"]:
tracking["requesterName"] = module.params["requester_name"]
if module.params["requester_email"]:
tracking["requesterEmail"] = module.params["requester_email"]
if module.params["requester_phone"]:
tracking["requesterPhone"] = module.params["requester_phone"]
if module.params["tracking_info"]:
tracking["trackingInfo"] = module.params["tracking_info"]
if module.params["custom_fields"]:
# Omit custom fields from submitted dict if not present, instead of submitting them with value of 'null'
# The ECS API does technically accept null without error, but it complicates debugging user escalations and is unnecessary bandwidth.
custom_fields = {}
for k, v in module.params['custom_fields'].items():
for k, v in module.params["custom_fields"].items():
if v is not None:
custom_fields[k] = v
tracking['customFields'] = custom_fields
if module.params['additional_emails']:
tracking['additionalEmails'] = module.params['additional_emails']
body['tracking'] = tracking
tracking["customFields"] = custom_fields
if module.params["additional_emails"]:
tracking["additionalEmails"] = module.params["additional_emails"]
body["tracking"] = tracking
return body
def convert_cert_subject_params(self, module):
body = {}
if module.params['subject_alt_name']:
body['subjectAltName'] = module.params['subject_alt_name']
if module.params['org']:
body['org'] = module.params['org']
if module.params['ou']:
body['ou'] = module.params['ou']
if module.params["subject_alt_name"]:
body["subjectAltName"] = module.params["subject_alt_name"]
if module.params["org"]:
body["org"] = module.params["org"]
if module.params["ou"]:
body["ou"] = module.params["ou"]
return body
def convert_general_params(self, module):
body = {}
if module.params['eku']:
body['eku'] = module.params['eku']
if self.request_type == 'new':
body['certType'] = module.params['cert_type']
body['clientId'] = module.params['client_id']
body.update(convert_module_param_to_json_bool(module, 'ctLog', 'ct_log'))
body.update(convert_module_param_to_json_bool(module, 'endUserKeyStorageAgreement', 'end_user_key_storage_agreement'))
if module.params["eku"]:
body["eku"] = module.params["eku"]
if self.request_type == "new":
body["certType"] = module.params["cert_type"]
body["clientId"] = module.params["client_id"]
body.update(convert_module_param_to_json_bool(module, "ctLog", "ct_log"))
body.update(
convert_module_param_to_json_bool(
module, "endUserKeyStorageAgreement", "end_user_key_storage_agreement"
)
)
return body
def convert_expiry_params(self, module):
body = {}
if module.params['cert_lifetime']:
body['certLifetime'] = module.params['cert_lifetime']
elif module.params['cert_expiry']:
body['certExpiryDate'] = module.params['cert_expiry']
if module.params["cert_lifetime"]:
body["certLifetime"] = module.params["cert_lifetime"]
elif module.params["cert_expiry"]:
body["certExpiryDate"] = module.params["cert_expiry"]
# If neither cerTLifetime or certExpiryDate was specified and the request type is new, default to 365 days
elif self.request_type != 'reissue':
elif self.request_type != "reissue":
gmt_now = datetime.datetime.fromtimestamp(time.mktime(time.gmtime()))
expiry = gmt_now + datetime.timedelta(days=365)
body['certExpiryDate'] = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
body["certExpiryDate"] = expiry.strftime("%Y-%m-%dT%H:%M:%S.00Z")
return body
def set_tracking_id_by_serial_number(self, module):
@@ -729,21 +750,30 @@ class EcsCertificate(object):
# Use serial_number to identify if certificate is an Entrust Certificate
# with an associated tracking ID
serial_number = "{0:X}".format(self.cert.serial_number)
cert_results = self.ecs_client.GetCertificates(serialNumber=serial_number).get('certificates', {})
cert_results = self.ecs_client.GetCertificates(
serialNumber=serial_number
).get("certificates", {})
if len(cert_results) == 1:
self.tracking_id = cert_results[0].get('trackingId')
self.tracking_id = cert_results[0].get("trackingId")
except RestOperationException:
# If we fail to find a cert by serial number, that's fine, we just do not set self.tracking_id
return
def set_cert_details(self, module):
try:
self.cert_details = self.ecs_client.GetCertificate(trackingId=self.tracking_id)
self.cert_status = self.cert_details.get('status')
self.serial_number = self.cert_details.get('serialNumber')
self.cert_days = calculate_cert_days(self.cert_details.get('expiresAfter'))
self.cert_details = self.ecs_client.GetCertificate(
trackingId=self.tracking_id
)
self.cert_status = self.cert_details.get("status")
self.serial_number = self.cert_details.get("serialNumber")
self.cert_days = calculate_cert_days(self.cert_details.get("expiresAfter"))
except RestOperationException as e:
module.fail_json('Failed to get details of certificate with tracking_id="{0}", Error: '.format(self.tracking_id), to_native(e.message))
module.fail_json(
'Failed to get details of certificate with tracking_id="{0}", Error: '.format(
self.tracking_id
),
to_native(e.message),
)
def check(self, module):
if self.cert:
@@ -751,22 +781,34 @@ class EcsCertificate(object):
# We will only set updated tracking ID based on certificate in "path" if it is managed by entrust.
self.set_tracking_id_by_serial_number(module)
if module.params['tracking_id'] and self.tracking_id and module.params['tracking_id'] != self.tracking_id:
module.warn('tracking_id parameter of "{0}" provided, but will be ignored. Valid certificate was present in path "{1}" with '
'tracking_id of "{2}".'.format(module.params['tracking_id'], self.path, self.tracking_id))
if (
module.params["tracking_id"]
and self.tracking_id
and module.params["tracking_id"] != self.tracking_id
):
module.warn(
'tracking_id parameter of "{0}" provided, but will be ignored. Valid certificate was present in path "{1}" with '
'tracking_id of "{2}".'.format(
module.params["tracking_id"], self.path, self.tracking_id
)
)
# If we did not end up setting tracking_id based on existing cert, get from module params
if not self.tracking_id:
self.tracking_id = module.params['tracking_id']
self.tracking_id = module.params["tracking_id"]
if not self.tracking_id:
return False
self.set_cert_details(module)
if self.cert_status == 'EXPIRED' or self.cert_status == 'SUSPENDED' or self.cert_status == 'REVOKED':
if (
self.cert_status == "EXPIRED"
or self.cert_status == "SUSPENDED"
or self.cert_status == "REVOKED"
):
return False
if self.cert_days < module.params['remaining_days']:
if self.cert_days < module.params["remaining_days"]:
return False
return True
@@ -777,20 +819,25 @@ class EcsCertificate(object):
# Read the CSR contents
if self.csr and os.path.exists(self.csr):
with open(self.csr, 'r') as csr_file:
body['csr'] = csr_file.read()
with open(self.csr, "r") as csr_file:
body["csr"] = csr_file.read()
# Check if the path is already a cert
# tracking_id may be set as a parameter or by get_cert_details if an entrust cert is in 'path'. If tracking ID is null
# We will be performing a reissue operation.
if self.request_type != 'new' and not self.tracking_id:
module.warn('No existing Entrust certificate found in path={0} and no tracking_id was provided, setting request_type to "new" for this task'
'run. Future playbook runs that point to the pathination file in {1} will use request_type={2}'
.format(self.path, self.path, self.request_type))
self.request_type = 'new'
elif self.request_type == 'new' and self.tracking_id:
module.warn('Existing certificate being acted upon, but request_type is "new", so will be a new certificate issuance rather than a'
'reissue or renew')
if self.request_type != "new" and not self.tracking_id:
module.warn(
'No existing Entrust certificate found in path={0} and no tracking_id was provided, setting request_type to "new" for this task'
"run. Future playbook runs that point to the pathination file in {1} will use request_type={2}".format(
self.path, self.path, self.request_type
)
)
self.request_type = "new"
elif self.request_type == "new" and self.tracking_id:
module.warn(
'Existing certificate being acted upon, but request_type is "new", so will be a new certificate issuance rather than a'
"reissue or renew"
)
# Use cases where request type is new and no existing certificate, or where request type is reissue/renew and a valid
# existing certificate is found, do not need warnings.
@@ -801,135 +848,161 @@ class EcsCertificate(object):
if not module.check_mode:
try:
if self.request_type == 'validate_only':
body['validateOnly'] = 'true'
if self.request_type == "validate_only":
body["validateOnly"] = "true"
result = self.ecs_client.NewCertRequest(Body=body)
if self.request_type == 'new':
if self.request_type == "new":
result = self.ecs_client.NewCertRequest(Body=body)
elif self.request_type == 'renew':
result = self.ecs_client.RenewCertRequest(trackingId=self.tracking_id, Body=body)
elif self.request_type == 'reissue':
result = self.ecs_client.ReissueCertRequest(trackingId=self.tracking_id, Body=body)
self.tracking_id = result.get('trackingId')
elif self.request_type == "renew":
result = self.ecs_client.RenewCertRequest(
trackingId=self.tracking_id, Body=body
)
elif self.request_type == "reissue":
result = self.ecs_client.ReissueCertRequest(
trackingId=self.tracking_id, Body=body
)
self.tracking_id = result.get("trackingId")
self.set_cert_details(module)
except RestOperationException as e:
module.fail_json(msg='Failed to request new certificate from Entrust (ECS) {0}'.format(e.message))
module.fail_json(
msg="Failed to request new certificate from Entrust (ECS) {0}".format(
e.message
)
)
if self.request_type != 'validate_only':
if self.request_type != "validate_only":
if self.backup:
self.backup_file = module.backup_local(self.path)
write_file(module, to_bytes(self.cert_details.get('endEntityCert')))
if self.full_chain_path and self.cert_details.get('chainCerts'):
write_file(module, to_bytes(self.cert_details.get("endEntityCert")))
if self.full_chain_path and self.cert_details.get("chainCerts"):
if self.backup:
self.backup_full_chain_file = module.backup_local(self.full_chain_path)
chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n'
write_file(module, to_bytes(chain_string), path=self.full_chain_path)
self.backup_full_chain_file = module.backup_local(
self.full_chain_path
)
chain_string = (
"\n".join(self.cert_details.get("chainCerts")) + "\n"
)
write_file(
module, to_bytes(chain_string), path=self.full_chain_path
)
self.changed = True
# If there is no certificate present in path but a tracking ID was specified, save it to disk
elif not os.path.exists(self.path) and self.tracking_id:
if not module.check_mode:
write_file(module, to_bytes(self.cert_details.get('endEntityCert')))
if self.full_chain_path and self.cert_details.get('chainCerts'):
chain_string = '\n'.join(self.cert_details.get('chainCerts')) + '\n'
write_file(module, to_bytes(chain_string), path=self.full_chain_path)
write_file(module, to_bytes(self.cert_details.get("endEntityCert")))
if self.full_chain_path and self.cert_details.get("chainCerts"):
chain_string = "\n".join(self.cert_details.get("chainCerts")) + "\n"
write_file(
module, to_bytes(chain_string), path=self.full_chain_path
)
self.changed = True
def dump(self):
result = {
'changed': self.changed,
'filename': self.path,
'tracking_id': self.tracking_id,
'cert_status': self.cert_status,
'serial_number': self.serial_number,
'cert_days': self.cert_days,
'cert_details': self.cert_details,
"changed": self.changed,
"filename": self.path,
"tracking_id": self.tracking_id,
"cert_status": self.cert_status,
"serial_number": self.serial_number,
"cert_days": self.cert_days,
"cert_details": self.cert_details,
}
if self.backup_file:
result['backup_file'] = self.backup_file
result['backup_full_chain_file'] = self.backup_full_chain_file
result["backup_file"] = self.backup_file
result["backup_full_chain_file"] = self.backup_full_chain_file
return result
def custom_fields_spec():
return dict(
text1=dict(type='str'),
text2=dict(type='str'),
text3=dict(type='str'),
text4=dict(type='str'),
text5=dict(type='str'),
text6=dict(type='str'),
text7=dict(type='str'),
text8=dict(type='str'),
text9=dict(type='str'),
text10=dict(type='str'),
text11=dict(type='str'),
text12=dict(type='str'),
text13=dict(type='str'),
text14=dict(type='str'),
text15=dict(type='str'),
number1=dict(type='float'),
number2=dict(type='float'),
number3=dict(type='float'),
number4=dict(type='float'),
number5=dict(type='float'),
date1=dict(type='str'),
date2=dict(type='str'),
date3=dict(type='str'),
date4=dict(type='str'),
date5=dict(type='str'),
email1=dict(type='str'),
email2=dict(type='str'),
email3=dict(type='str'),
email4=dict(type='str'),
email5=dict(type='str'),
dropdown1=dict(type='str'),
dropdown2=dict(type='str'),
dropdown3=dict(type='str'),
dropdown4=dict(type='str'),
dropdown5=dict(type='str'),
text1=dict(type="str"),
text2=dict(type="str"),
text3=dict(type="str"),
text4=dict(type="str"),
text5=dict(type="str"),
text6=dict(type="str"),
text7=dict(type="str"),
text8=dict(type="str"),
text9=dict(type="str"),
text10=dict(type="str"),
text11=dict(type="str"),
text12=dict(type="str"),
text13=dict(type="str"),
text14=dict(type="str"),
text15=dict(type="str"),
number1=dict(type="float"),
number2=dict(type="float"),
number3=dict(type="float"),
number4=dict(type="float"),
number5=dict(type="float"),
date1=dict(type="str"),
date2=dict(type="str"),
date3=dict(type="str"),
date4=dict(type="str"),
date5=dict(type="str"),
email1=dict(type="str"),
email2=dict(type="str"),
email3=dict(type="str"),
email4=dict(type="str"),
email5=dict(type="str"),
dropdown1=dict(type="str"),
dropdown2=dict(type="str"),
dropdown3=dict(type="str"),
dropdown4=dict(type="str"),
dropdown5=dict(type="str"),
)
def ecs_certificate_argument_spec():
return dict(
backup=dict(type='bool', default=False),
force=dict(type='bool', default=False),
path=dict(type='path', required=True),
full_chain_path=dict(type='path'),
tracking_id=dict(type='int'),
remaining_days=dict(type='int', default=30),
request_type=dict(type='str', default='new', choices=['new', 'renew', 'reissue', 'validate_only']),
cert_type=dict(type='str', choices=['STANDARD_SSL',
'ADVANTAGE_SSL',
'UC_SSL',
'EV_SSL',
'WILDCARD_SSL',
'PRIVATE_SSL',
'PD_SSL',
'CODE_SIGNING',
'EV_CODE_SIGNING',
'CDS_INDIVIDUAL',
'CDS_GROUP',
'CDS_ENT_LITE',
'CDS_ENT_PRO',
'SMIME_ENT',
]),
csr=dict(type='str'),
subject_alt_name=dict(type='list', elements='str'),
eku=dict(type='str', choices=['SERVER_AUTH', 'CLIENT_AUTH', 'SERVER_AND_CLIENT_AUTH']),
ct_log=dict(type='bool'),
client_id=dict(type='int', default=1),
org=dict(type='str'),
ou=dict(type='list', elements='str'),
end_user_key_storage_agreement=dict(type='bool'),
tracking_info=dict(type='str'),
requester_name=dict(type='str', required=True),
requester_email=dict(type='str', required=True),
requester_phone=dict(type='str', required=True),
additional_emails=dict(type='list', elements='str'),
custom_fields=dict(type='dict', default=None, options=custom_fields_spec()),
cert_expiry=dict(type='str'),
cert_lifetime=dict(type='str', choices=['P1Y', 'P2Y', 'P3Y']),
backup=dict(type="bool", default=False),
force=dict(type="bool", default=False),
path=dict(type="path", required=True),
full_chain_path=dict(type="path"),
tracking_id=dict(type="int"),
remaining_days=dict(type="int", default=30),
request_type=dict(
type="str",
default="new",
choices=["new", "renew", "reissue", "validate_only"],
),
cert_type=dict(
type="str",
choices=[
"STANDARD_SSL",
"ADVANTAGE_SSL",
"UC_SSL",
"EV_SSL",
"WILDCARD_SSL",
"PRIVATE_SSL",
"PD_SSL",
"CODE_SIGNING",
"EV_CODE_SIGNING",
"CDS_INDIVIDUAL",
"CDS_GROUP",
"CDS_ENT_LITE",
"CDS_ENT_PRO",
"SMIME_ENT",
],
),
csr=dict(type="str"),
subject_alt_name=dict(type="list", elements="str"),
eku=dict(
type="str", choices=["SERVER_AUTH", "CLIENT_AUTH", "SERVER_AND_CLIENT_AUTH"]
),
ct_log=dict(type="bool"),
client_id=dict(type="int", default=1),
org=dict(type="str"),
ou=dict(type="list", elements="str"),
end_user_key_storage_agreement=dict(type="bool"),
tracking_info=dict(type="str"),
requester_name=dict(type="str", required=True),
requester_email=dict(type="str", required=True),
requester_phone=dict(type="str", required=True),
additional_emails=dict(type="list", elements="str"),
custom_fields=dict(type="dict", default=None, options=custom_fields_spec()),
cert_expiry=dict(type="str"),
cert_lifetime=dict(type="str", choices=["P1Y", "P2Y", "P3Y"]),
)
@@ -939,54 +1012,91 @@ def main():
module = AnsibleModule(
argument_spec=ecs_argument_spec,
required_if=(
['request_type', 'new', ['cert_type']],
['request_type', 'validate_only', ['cert_type']],
['cert_type', 'CODE_SIGNING', ['end_user_key_storage_agreement']],
['cert_type', 'EV_CODE_SIGNING', ['end_user_key_storage_agreement']],
),
mutually_exclusive=(
['cert_expiry', 'cert_lifetime'],
["request_type", "new", ["cert_type"]],
["request_type", "validate_only", ["cert_type"]],
["cert_type", "CODE_SIGNING", ["end_user_key_storage_agreement"]],
["cert_type", "EV_CODE_SIGNING", ["end_user_key_storage_agreement"]],
),
mutually_exclusive=(["cert_expiry", "cert_lifetime"],),
supports_check_mode=True,
)
if not CRYPTOGRAPHY_FOUND or CRYPTOGRAPHY_VERSION < LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION):
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
if not CRYPTOGRAPHY_FOUND or CRYPTOGRAPHY_VERSION < LooseVersion(
MINIMAL_CRYPTOGRAPHY_VERSION
):
module.fail_json(
msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
# If validate_only is used, pointing to an existing tracking_id is an invalid operation
if module.params['tracking_id']:
if module.params['request_type'] == 'new' or module.params['request_type'] == 'validate_only':
module.fail_json(msg='The tracking_id field is invalid when request_type="{0}".'.format(module.params['request_type']))
if module.params["tracking_id"]:
if (
module.params["request_type"] == "new"
or module.params["request_type"] == "validate_only"
):
module.fail_json(
msg='The tracking_id field is invalid when request_type="{0}".'.format(
module.params["request_type"]
)
)
# A reissued request can not specify an expiration date or lifetime
if module.params['request_type'] == 'reissue':
if module.params['cert_expiry']:
module.fail_json(msg='The cert_expiry field is invalid when request_type="reissue".')
elif module.params['cert_lifetime']:
module.fail_json(msg='The cert_lifetime field is invalid when request_type="reissue".')
if module.params["request_type"] == "reissue":
if module.params["cert_expiry"]:
module.fail_json(
msg='The cert_expiry field is invalid when request_type="reissue".'
)
elif module.params["cert_lifetime"]:
module.fail_json(
msg='The cert_lifetime field is invalid when request_type="reissue".'
)
# Reissued or renew request can omit the CSR
elif module.params['request_type'] != 'renew':
module_params_csr = module.params['csr']
elif module.params["request_type"] != "renew":
module_params_csr = module.params["csr"]
if module_params_csr is None:
module.fail_json(msg='The csr field is required when request_type={0}'.format(module.params['request_type']))
module.fail_json(
msg="The csr field is required when request_type={0}".format(
module.params["request_type"]
)
)
elif not os.path.exists(module_params_csr):
module.fail_json(msg='The csr field of {0} was not a valid path. csr is required when request_type={1}'.format(
module_params_csr, module.params['request_type']))
module.fail_json(
msg="The csr field of {0} was not a valid path. csr is required when request_type={1}".format(
module_params_csr, module.params["request_type"]
)
)
if module.params['ou'] and len(module.params['ou']) > 1:
if module.params["ou"] and len(module.params["ou"]) > 1:
module.fail_json(msg='Multiple "ou" values are not currently supported.')
if module.params['end_user_key_storage_agreement']:
if module.params['cert_type'] != 'CODE_SIGNING' and module.params['cert_type'] != 'EV_CODE_SIGNING':
module.fail_json(msg='Parameter "end_user_key_storage_agreement" is valid only for cert_types "CODE_SIGNING" and "EV_CODE_SIGNING"')
if module.params["end_user_key_storage_agreement"]:
if (
module.params["cert_type"] != "CODE_SIGNING"
and module.params["cert_type"] != "EV_CODE_SIGNING"
):
module.fail_json(
msg='Parameter "end_user_key_storage_agreement" is valid only for cert_types "CODE_SIGNING" and "EV_CODE_SIGNING"'
)
if module.params['org'] and module.params['client_id'] != 1 and module.params['cert_type'] != 'PD_SSL':
module.fail_json(msg='The "org" parameter is not supported when client_id parameter is set to a value other than 1, unless cert_type is "PD_SSL".')
if (
module.params["org"]
and module.params["client_id"] != 1
and module.params["cert_type"] != "PD_SSL"
):
module.fail_json(
msg='The "org" parameter is not supported when client_id parameter is set to a value other than 1, unless cert_type is "PD_SSL".'
)
if module.params['cert_expiry']:
if not validate_cert_expiry(module.params['cert_expiry']):
module.fail_json(msg='The "cert_expiry" parameter of "{0}" is not a valid date or date-time'.format(module.params['cert_expiry']))
if module.params["cert_expiry"]:
if not validate_cert_expiry(module.params["cert_expiry"]):
module.fail_json(
msg='The "cert_expiry" parameter of "{0}" is not a valid date or date-time'.format(
module.params["cert_expiry"]
)
)
certificate = EcsCertificate(module)
certificate.request_cert(module)
@@ -994,5 +1104,5 @@ def main():
module.exit_json(**result)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -237,15 +237,15 @@ from ansible_collections.community.crypto.plugins.module_utils.ecs.api import (
def calculate_days_remaining(expiry_date):
days_remaining = None
if expiry_date:
expiry_datetime = datetime.datetime.strptime(expiry_date, '%Y-%m-%dT%H:%M:%SZ')
expiry_datetime = datetime.datetime.strptime(expiry_date, "%Y-%m-%dT%H:%M:%SZ")
days_remaining = (expiry_datetime - datetime.datetime.now()).days
return days_remaining
class EcsDomain(object):
'''
"""
Entrust Certificate Services domain class.
'''
"""
def __init__(self, module):
self.changed = False
@@ -270,53 +270,76 @@ class EcsDomain(object):
# Instantiate the ECS client and then try a no-op connection to verify credentials are valid
try:
self.ecs_client = ECSClient(
entrust_api_user=module.params['entrust_api_user'],
entrust_api_key=module.params['entrust_api_key'],
entrust_api_cert=module.params['entrust_api_client_cert_path'],
entrust_api_cert_key=module.params['entrust_api_client_cert_key_path'],
entrust_api_specification_path=module.params['entrust_api_specification_path']
entrust_api_user=module.params["entrust_api_user"],
entrust_api_key=module.params["entrust_api_key"],
entrust_api_cert=module.params["entrust_api_client_cert_path"],
entrust_api_cert_key=module.params["entrust_api_client_cert_key_path"],
entrust_api_specification_path=module.params[
"entrust_api_specification_path"
],
)
except SessionConfigurationException as e:
module.fail_json(msg='Failed to initialize Entrust Provider: {0}'.format(to_native(e)))
module.fail_json(
msg="Failed to initialize Entrust Provider: {0}".format(to_native(e))
)
try:
self.ecs_client.GetAppVersion()
except RestOperationException as e:
module.fail_json(msg='Please verify credential information. Received exception when testing ECS connection: {0}'.format(to_native(e.message)))
module.fail_json(
msg="Please verify credential information. Received exception when testing ECS connection: {0}".format(
to_native(e.message)
)
)
def set_domain_details(self, domain_details):
if domain_details.get('verificationMethod'):
self.verification_method = domain_details['verificationMethod'].lower()
self.domain_status = domain_details['verificationStatus']
self.ov_eligible = domain_details.get('ovEligible')
self.ov_days_remaining = calculate_days_remaining(domain_details.get('ovExpiry'))
self.ev_eligible = domain_details.get('evEligible')
self.ev_days_remaining = calculate_days_remaining(domain_details.get('evExpiry'))
self.client_id = domain_details['clientId']
if domain_details.get("verificationMethod"):
self.verification_method = domain_details["verificationMethod"].lower()
self.domain_status = domain_details["verificationStatus"]
self.ov_eligible = domain_details.get("ovEligible")
self.ov_days_remaining = calculate_days_remaining(
domain_details.get("ovExpiry")
)
self.ev_eligible = domain_details.get("evEligible")
self.ev_days_remaining = calculate_days_remaining(
domain_details.get("evExpiry")
)
self.client_id = domain_details["clientId"]
if self.verification_method == 'dns' and domain_details.get('dnsMethod'):
self.dns_location = domain_details['dnsMethod']['recordDomain']
self.dns_resource_type = domain_details['dnsMethod']['recordType']
self.dns_contents = domain_details['dnsMethod']['recordValue']
elif self.verification_method == 'web_server' and domain_details.get('webServerMethod'):
self.file_location = domain_details['webServerMethod']['fileLocation']
self.file_contents = domain_details['webServerMethod']['fileContents']
elif self.verification_method == 'email' and domain_details.get('emailMethod'):
self.emails = domain_details['emailMethod']
if self.verification_method == "dns" and domain_details.get("dnsMethod"):
self.dns_location = domain_details["dnsMethod"]["recordDomain"]
self.dns_resource_type = domain_details["dnsMethod"]["recordType"]
self.dns_contents = domain_details["dnsMethod"]["recordValue"]
elif self.verification_method == "web_server" and domain_details.get(
"webServerMethod"
):
self.file_location = domain_details["webServerMethod"]["fileLocation"]
self.file_contents = domain_details["webServerMethod"]["fileContents"]
elif self.verification_method == "email" and domain_details.get("emailMethod"):
self.emails = domain_details["emailMethod"]
def check(self, module):
try:
domain_details = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name'])
domain_details = self.ecs_client.GetDomain(
clientId=module.params["client_id"], domain=module.params["domain_name"]
)
self.set_domain_details(domain_details)
if self.domain_status != 'APPROVED' and self.domain_status != 'INITIAL_VERIFICATION' and self.domain_status != 'RE_VERIFICATION':
if (
self.domain_status != "APPROVED"
and self.domain_status != "INITIAL_VERIFICATION"
and self.domain_status != "RE_VERIFICATION"
):
return False
# If domain verification is in process, we want to return the random values and treat it as a valid.
if self.domain_status == 'INITIAL_VERIFICATION' or self.domain_status == 'RE_VERIFICATION':
if (
self.domain_status == "INITIAL_VERIFICATION"
or self.domain_status == "RE_VERIFICATION"
):
# Unless the verification method has changed, in which case we need to do a reverify request.
if self.verification_method != module.params['verification_method']:
if self.verification_method != module.params["verification_method"]:
return False
if self.domain_status == 'EXPIRING':
if self.domain_status == "EXPIRING":
return False
return True
@@ -327,83 +350,112 @@ class EcsDomain(object):
if not self.check(module):
body = {}
body['verificationMethod'] = module.params['verification_method'].upper()
if module.params['verification_method'] == 'email':
body["verificationMethod"] = module.params["verification_method"].upper()
if module.params["verification_method"] == "email":
emailMethod = {}
if module.params['verification_email']:
emailMethod['emailSource'] = 'SPECIFIED'
emailMethod['email'] = module.params['verification_email']
if module.params["verification_email"]:
emailMethod["emailSource"] = "SPECIFIED"
emailMethod["email"] = module.params["verification_email"]
else:
emailMethod['emailSource'] = 'INCLUDE_WHOIS'
body['emailMethod'] = emailMethod
emailMethod["emailSource"] = "INCLUDE_WHOIS"
body["emailMethod"] = emailMethod
# Only populate domain name in body if it is not an existing domain
if not self.domain_status:
body['domainName'] = module.params['domain_name']
body["domainName"] = module.params["domain_name"]
try:
if not self.domain_status:
self.ecs_client.AddDomain(clientId=module.params['client_id'], Body=body)
self.ecs_client.AddDomain(
clientId=module.params["client_id"], Body=body
)
else:
self.ecs_client.ReverifyDomain(clientId=module.params['client_id'], domain=module.params['domain_name'], Body=body)
self.ecs_client.ReverifyDomain(
clientId=module.params["client_id"],
domain=module.params["domain_name"],
Body=body,
)
time.sleep(5)
result = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name'])
result = self.ecs_client.GetDomain(
clientId=module.params["client_id"],
domain=module.params["domain_name"],
)
# It takes a bit of time before the random values are available
if module.params['verification_method'] == 'dns' or module.params['verification_method'] == 'web_server':
if (
module.params["verification_method"] == "dns"
or module.params["verification_method"] == "web_server"
):
for i in range(4):
# Check both that random values are now available, and that they're different than were populated by previous 'check'
if module.params['verification_method'] == 'dns':
if result.get('dnsMethod') and result['dnsMethod']['recordValue'] != self.dns_contents:
if module.params["verification_method"] == "dns":
if (
result.get("dnsMethod")
and result["dnsMethod"]["recordValue"]
!= self.dns_contents
):
break
elif module.params['verification_method'] == 'web_server':
if result.get('webServerMethod') and result['webServerMethod']['fileContents'] != self.file_contents:
elif module.params["verification_method"] == "web_server":
if (
result.get("webServerMethod")
and result["webServerMethod"]["fileContents"]
!= self.file_contents
):
break
time.sleep(10)
result = self.ecs_client.GetDomain(clientId=module.params['client_id'], domain=module.params['domain_name'])
result = self.ecs_client.GetDomain(
clientId=module.params["client_id"],
domain=module.params["domain_name"],
)
self.changed = True
self.set_domain_details(result)
except RestOperationException as e:
module.fail_json(msg='Failed to request domain validation from Entrust (ECS) {0}'.format(e.message))
module.fail_json(
msg="Failed to request domain validation from Entrust (ECS) {0}".format(
e.message
)
)
def dump(self):
result = {
'changed': self.changed,
'client_id': self.client_id,
'domain_status': self.domain_status,
"changed": self.changed,
"client_id": self.client_id,
"domain_status": self.domain_status,
}
if self.verification_method:
result['verification_method'] = self.verification_method
result["verification_method"] = self.verification_method
if self.ov_eligible is not None:
result['ov_eligible'] = self.ov_eligible
result["ov_eligible"] = self.ov_eligible
if self.ov_days_remaining:
result['ov_days_remaining'] = self.ov_days_remaining
result["ov_days_remaining"] = self.ov_days_remaining
if self.ev_eligible is not None:
result['ev_eligible'] = self.ev_eligible
result["ev_eligible"] = self.ev_eligible
if self.ev_days_remaining:
result['ev_days_remaining'] = self.ev_days_remaining
result["ev_days_remaining"] = self.ev_days_remaining
if self.emails:
result['emails'] = self.emails
result["emails"] = self.emails
if self.verification_method == 'dns':
result['dns_location'] = self.dns_location
result['dns_contents'] = self.dns_contents
result['dns_resource_type'] = self.dns_resource_type
elif self.verification_method == 'web_server':
result['file_location'] = self.file_location
result['file_contents'] = self.file_contents
elif self.verification_method == 'email':
result['emails'] = self.emails
if self.verification_method == "dns":
result["dns_location"] = self.dns_location
result["dns_contents"] = self.dns_contents
result["dns_resource_type"] = self.dns_resource_type
elif self.verification_method == "web_server":
result["file_location"] = self.file_location
result["file_contents"] = self.file_contents
elif self.verification_method == "email":
result["emails"] = self.emails
return result
def ecs_domain_argument_spec():
return dict(
client_id=dict(type='int', default=1),
domain_name=dict(type='str', required=True),
verification_method=dict(type='str', required=True, choices=['dns', 'email', 'manual', 'web_server']),
verification_email=dict(type='str'),
client_id=dict(type="int", default=1),
domain_name=dict(type="str", required=True),
verification_method=dict(
type="str", required=True, choices=["dns", "email", "manual", "web_server"]
),
verification_email=dict(type="str"),
)
@@ -415,8 +467,15 @@ def main():
supports_check_mode=False,
)
if module.params['verification_email'] and module.params['verification_method'] != 'email':
module.fail_json(msg='The verification_email field is invalid when verification_method="{0}".'.format(module.params['verification_method']))
if (
module.params["verification_email"]
and module.params["verification_method"] != "email"
):
module.fail_json(
msg='The verification_email field is invalid when verification_method="{0}".'.format(
module.params["verification_method"]
)
)
domain = EcsDomain(module)
domain.request_domain(module)
@@ -424,5 +483,5 @@ def main():
module.exit_json(**result)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -294,7 +294,7 @@ from ansible_collections.community.crypto.plugins.module_utils.version import (
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
MINIMAL_CRYPTOGRAPHY_VERSION = "1.6"
CREATE_DEFAULT_CONTEXT_IMP_ERR = None
try:
@@ -311,6 +311,7 @@ try:
import cryptography.exceptions
import cryptography.x509
from cryptography.hazmat.backends import default_backend as cryptography_backend
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -320,85 +321,100 @@ else:
def send_starttls_packet(sock, server_type):
if server_type == 'mysql':
if server_type == "mysql":
ssl_request_packet = (
b'\x20\x00\x00\x01\x85\xae\x7f\x00' +
b'\x00\x00\x00\x01\x21\x00\x00\x00' +
b'\x00\x00\x00\x00\x00\x00\x00\x00' +
b'\x00\x00\x00\x00\x00\x00\x00\x00' +
b'\x00\x00\x00\x00'
b"\x20\x00\x00\x01\x85\xae\x7f\x00"
+ b"\x00\x00\x00\x01\x21\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00\x00\x00\x00\x00"
+ b"\x00\x00\x00\x00"
)
sock.recv(8192) # discard initial handshake from server for this naive implementation
sock.recv(
8192
) # discard initial handshake from server for this naive implementation
sock.send(ssl_request_packet)
def main():
module = AnsibleModule(
argument_spec=dict(
ca_cert=dict(type='path'),
host=dict(type='str', required=True),
port=dict(type='int', required=True),
proxy_host=dict(type='str'),
proxy_port=dict(type='int', default=8080),
server_name=dict(type='str'),
timeout=dict(type='int', default=10),
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
starttls=dict(type='str', choices=['mysql']),
ciphers=dict(type='list', elements='str'),
asn1_base64=dict(type='bool'),
tls_ctx_options=dict(type='list', elements='raw'),
get_certificate_chain=dict(type='bool', default=False),
ca_cert=dict(type="path"),
host=dict(type="str", required=True),
port=dict(type="int", required=True),
proxy_host=dict(type="str"),
proxy_port=dict(type="int", default=8080),
server_name=dict(type="str"),
timeout=dict(type="int", default=10),
select_crypto_backend=dict(
type="str", choices=["auto", "cryptography"], default="auto"
),
starttls=dict(type="str", choices=["mysql"]),
ciphers=dict(type="list", elements="str"),
asn1_base64=dict(type="bool"),
tls_ctx_options=dict(type="list", elements="raw"),
get_certificate_chain=dict(type="bool", default=False),
),
)
ca_cert = module.params.get('ca_cert')
host = module.params.get('host')
port = module.params.get('port')
proxy_host = module.params.get('proxy_host')
proxy_port = module.params.get('proxy_port')
timeout = module.params.get('timeout')
server_name = module.params.get('server_name')
start_tls_server_type = module.params.get('starttls')
ciphers = module.params.get('ciphers')
asn1_base64 = module.params['asn1_base64']
tls_ctx_options = module.params['tls_ctx_options']
get_certificate_chain = module.params['get_certificate_chain']
ca_cert = module.params.get("ca_cert")
host = module.params.get("host")
port = module.params.get("port")
proxy_host = module.params.get("proxy_host")
proxy_port = module.params.get("proxy_port")
timeout = module.params.get("timeout")
server_name = module.params.get("server_name")
start_tls_server_type = module.params.get("starttls")
ciphers = module.params.get("ciphers")
asn1_base64 = module.params["asn1_base64"]
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',
"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). '
'The Python version used to run the get_certificate module is %s' % sys.version
msg="get_certificate_chain=true can only be used with Python 3.10 (Python 3.13+ officially supports this). "
"The Python version used to run the get_certificate module is %s"
% sys.version
)
backend = module.params.get('select_crypto_backend')
if backend == 'auto':
backend = module.params.get("select_crypto_backend")
if backend == "auto":
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_cryptography = (
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
)
# Try cryptography
if can_use_cryptography:
backend = 'cryptography'
backend = "cryptography"
# Success?
if backend == 'auto':
module.fail_json(msg=("Cannot detect the required Python library "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == "auto":
module.fail_json(
msg=(
"Cannot detect the required Python library " "cryptography (>= {0})"
).format(MINIMAL_CRYPTOGRAPHY_VERSION)
)
if backend == 'cryptography':
if backend == "cryptography":
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
module.fail_json(
msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
result = dict(
changed=False,
@@ -417,19 +433,27 @@ def main():
if not HAS_CREATE_DEFAULT_CONTEXT:
# Python < 2.7.9
if proxy_host:
module.fail_json(msg='To use proxy_host, you must run the get_certificate module with Python 2.7 or newer.',
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR)
module.fail_json(
msg="To use proxy_host, you must run the get_certificate module with Python 2.7 or newer.",
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR,
)
if ciphers is not None:
module.fail_json(msg='To use ciphers, you must run the get_certificate module with Python 2.7 or newer.',
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR)
module.fail_json(
msg="To use ciphers, you must run the get_certificate module with Python 2.7 or newer.",
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR,
)
if tls_ctx_options is not None:
module.fail_json(msg='To use tls_ctx_options, you must run the get_certificate module with Python 2.7 or newer.',
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR)
module.fail_json(
msg="To use tls_ctx_options, you must run the get_certificate module with Python 2.7 or newer.",
exception=CREATE_DEFAULT_CONTEXT_IMP_ERR,
)
try:
# Note: get_server_certificate does not support SNI!
cert = get_server_certificate((host, port), ca_certs=ca_cert)
except Exception as e:
module.fail_json(msg="Failed to get cert from {0}:{1}, error: {2}".format(host, port, e))
module.fail_json(
msg="Failed to get cert from {0}:{1}, error: {2}".format(host, port, e)
)
else:
# Python >= 2.7.9
try:
@@ -478,21 +502,35 @@ def main():
tls_ctx_option_int = tls_ctx_option_attr
# If tls_ctx_option_attr is not an integer
else:
module.fail_json(msg="Failed to determine the numeric value for {0}".format(tls_ctx_option_str))
module.fail_json(
msg="Failed to determine the numeric value for {0}".format(
tls_ctx_option_str
)
)
# If the item is an integer
elif isinstance(tls_ctx_option, int):
# Set tls_ctx_option_int to the item value
tls_ctx_option_int = tls_ctx_option
# If the item is not a string nor integer
else:
module.fail_json(msg="tls_ctx_options must be a string or integer, got {0!r}".format(tls_ctx_option))
tls_ctx_option_int = 0 # make pylint happy; this code is actually unreachable
module.fail_json(
msg="tls_ctx_options must be a string or integer, got {0!r}".format(
tls_ctx_option
)
)
tls_ctx_option_int = (
0 # make pylint happy; this code is actually unreachable
)
try:
# Add the int value of the item to ctx options
ctx.options |= tls_ctx_option_int
except Exception:
module.fail_json(msg="Failed to add {0} to CTX options".format(tls_ctx_option_str or tls_ctx_option_int))
module.fail_json(
msg="Failed to add {0} to CTX options".format(
tls_ctx_option_str or tls_ctx_option_int
)
)
tls_sock = ctx.wrap_socket(sock, server_hostname=server_name or host)
cert = tls_sock.getpeercert(True)
@@ -511,7 +549,9 @@ def main():
ssl_obj = tls_sock._sslobj # This is of type ssl._ssl._SSLSocket
verified_der_chain = _convert_chain(ssl_obj.get_verified_chain())
unverified_der_chain = _convert_chain(ssl_obj.get_unverified_chain())
unverified_der_chain = _convert_chain(
ssl_obj.get_unverified_chain()
)
else:
# This works with Python 3.13+
@@ -521,70 +561,95 @@ def main():
# if they are not byte strings to work around this.
def _convert_chain(chain):
return [
c if isinstance(c, bytes) else c.public_bytes(ssl._ssl.ENCODING_DER)
(
c
if isinstance(c, bytes)
else c.public_bytes(ssl._ssl.ENCODING_DER)
)
for c in chain
]
verified_der_chain = _convert_chain(tls_sock.get_verified_chain())
unverified_der_chain = _convert_chain(tls_sock.get_unverified_chain())
unverified_der_chain = _convert_chain(
tls_sock.get_unverified_chain()
)
verified_chain = [DER_cert_to_PEM_cert(c) for c in verified_der_chain]
unverified_chain = [DER_cert_to_PEM_cert(c) for c in unverified_der_chain]
unverified_chain = [
DER_cert_to_PEM_cert(c) for c in unverified_der_chain
]
except Exception as e:
if proxy_host:
module.fail_json(msg="Failed to get cert via proxy {0}:{1} from {2}:{3}, error: {4}".format(
proxy_host, proxy_port, host, port, e))
module.fail_json(
msg="Failed to get cert via proxy {0}:{1} from {2}:{3}, error: {4}".format(
proxy_host, proxy_port, host, port, e
)
)
else:
module.fail_json(msg="Failed to get cert from {0}:{1}, error: {2}".format(host, port, e))
module.fail_json(
msg="Failed to get cert from {0}:{1}, error: {2}".format(
host, port, e
)
)
result['cert'] = cert
result["cert"] = cert
if backend == 'cryptography':
x509 = cryptography.x509.load_pem_x509_certificate(to_bytes(cert), cryptography_backend())
result['subject'] = {}
if backend == "cryptography":
x509 = cryptography.x509.load_pem_x509_certificate(
to_bytes(cert), cryptography_backend()
)
result["subject"] = {}
for attribute in x509.subject:
result['subject'][cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value
result["subject"][cryptography_oid_to_name(attribute.oid, short=True)] = (
attribute.value
)
result['expired'] = get_not_valid_after(x509) < get_now_datetime(with_timezone=CRYPTOGRAPHY_TIMEZONE)
result["expired"] = get_not_valid_after(x509) < get_now_datetime(
with_timezone=CRYPTOGRAPHY_TIMEZONE
)
result['extensions'] = []
result["extensions"] = []
for dotted_number, entry in cryptography_get_extensions_from_cert(x509).items():
oid = cryptography.x509.oid.ObjectIdentifier(dotted_number)
ext = {
'critical': entry['critical'],
'asn1_data': entry['value'],
'name': cryptography_oid_to_name(oid, short=True),
"critical": entry["critical"],
"asn1_data": entry["value"],
"name": cryptography_oid_to_name(oid, short=True),
}
if not asn1_base64:
ext['asn1_data'] = base64.b64decode(ext['asn1_data'])
result['extensions'].append(ext)
ext["asn1_data"] = base64.b64decode(ext["asn1_data"])
result["extensions"].append(ext)
result['issuer'] = {}
result["issuer"] = {}
for attribute in x509.issuer:
result['issuer'][cryptography_oid_to_name(attribute.oid, short=True)] = attribute.value
result["issuer"][cryptography_oid_to_name(attribute.oid, short=True)] = (
attribute.value
)
result['not_after'] = get_not_valid_after(x509).strftime('%Y%m%d%H%M%SZ')
result['not_before'] = get_not_valid_before(x509).strftime('%Y%m%d%H%M%SZ')
result["not_after"] = get_not_valid_after(x509).strftime("%Y%m%d%H%M%SZ")
result["not_before"] = get_not_valid_before(x509).strftime("%Y%m%d%H%M%SZ")
result['serial_number'] = x509.serial_number
result['signature_algorithm'] = cryptography_oid_to_name(x509.signature_algorithm_oid)
result["serial_number"] = x509.serial_number
result["signature_algorithm"] = cryptography_oid_to_name(
x509.signature_algorithm_oid
)
# We need the -1 offset to get the same values as pyOpenSSL
if x509.version == cryptography.x509.Version.v1:
result['version'] = 1 - 1
result["version"] = 1 - 1
elif x509.version == cryptography.x509.Version.v3:
result['version'] = 3 - 1
result["version"] = 3 - 1
else:
result['version'] = "unknown"
result["version"] = "unknown"
if verified_chain is not None:
result['verified_chain'] = verified_chain
result["verified_chain"] = verified_chain
if unverified_chain is not None:
result['unverified_chain'] = unverified_chain
result["unverified_chain"] = unverified_chain
module.exit_json(**result)
if __name__ == '__main__':
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -312,25 +312,29 @@ class Certificate(OpensshModule):
super(Certificate, self).__init__(module)
self.ssh_keygen = KeygenCommand(self.module)
self.identifier = self.module.params['identifier'] or ""
self.options = self.module.params['options'] or []
self.path = self.module.params['path']
self.pkcs11_provider = self.module.params['pkcs11_provider']
self.principals = self.module.params['principals'] or []
self.public_key = self.module.params['public_key']
self.regenerate = self.module.params['regenerate'] if not self.module.params['force'] else 'always'
self.serial_number = self.module.params['serial_number']
self.signature_algorithm = self.module.params['signature_algorithm']
self.signing_key = self.module.params['signing_key']
self.state = self.module.params['state']
self.type = self.module.params['type']
self.use_agent = self.module.params['use_agent']
self.valid_at = self.module.params['valid_at']
self.ignore_timestamps = self.module.params['ignore_timestamps']
self.identifier = self.module.params["identifier"] or ""
self.options = self.module.params["options"] or []
self.path = self.module.params["path"]
self.pkcs11_provider = self.module.params["pkcs11_provider"]
self.principals = self.module.params["principals"] or []
self.public_key = self.module.params["public_key"]
self.regenerate = (
self.module.params["regenerate"]
if not self.module.params["force"]
else "always"
)
self.serial_number = self.module.params["serial_number"]
self.signature_algorithm = self.module.params["signature_algorithm"]
self.signing_key = self.module.params["signing_key"]
self.state = self.module.params["state"]
self.type = self.module.params["type"]
self.use_agent = self.module.params["use_agent"]
self.valid_at = self.module.params["valid_at"]
self.ignore_timestamps = self.module.params["ignore_timestamps"]
self._check_if_base_dir(self.path)
if self.state == 'present':
if self.state == "present":
self._validate_parameters()
self.data = None
@@ -339,7 +343,7 @@ class Certificate(OpensshModule):
self._load_certificate()
self.time_parameters = None
if self.state == 'present':
if self.state == "present":
self._set_time_parameters()
def _validate_parameters(self):
@@ -347,7 +351,9 @@ class Certificate(OpensshModule):
self._check_if_base_dir(path)
if self.options and self.type == "host":
self.module.fail_json(msg="Options can only be used with user certificates.")
self.module.fail_json(
msg="Options can only be used with user certificates."
)
if self.use_agent:
self._use_agent_available()
@@ -358,8 +364,8 @@ class Certificate(OpensshModule):
self.module.fail_json(msg="Failed to determine ssh version")
elif LooseVersion(ssh_version) < LooseVersion("7.6"):
self.module.fail_json(
msg="Signing with CA key in ssh agent requires ssh 7.6 or newer." +
" Your version is: %s" % ssh_version
msg="Signing with CA key in ssh agent requires ssh 7.6 or newer."
+ " Your version is: %s" % ssh_version
)
def _exists(self):
@@ -369,21 +375,23 @@ class Certificate(OpensshModule):
try:
self.original_data = OpensshCertificate.load(self.path)
except (TypeError, ValueError) as e:
if self.regenerate in ('never', 'fail'):
self.module.fail_json(msg="Unable to read existing certificate: %s" % to_native(e))
if self.regenerate in ("never", "fail"):
self.module.fail_json(
msg="Unable to read existing certificate: %s" % to_native(e)
)
self.module.warn("Unable to read existing certificate: %s" % to_native(e))
def _set_time_parameters(self):
try:
self.time_parameters = OpensshCertificateTimeParameters(
valid_from=self.module.params['valid_from'],
valid_to=self.module.params['valid_to'],
valid_from=self.module.params["valid_from"],
valid_to=self.module.params["valid_to"],
)
except ValueError as e:
self.module.fail_json(msg=to_native(e))
def _execute(self):
if self.state == 'present':
if self.state == "present":
if self._should_generate():
self._generate()
self._update_permissions(self.path)
@@ -392,44 +400,58 @@ class Certificate(OpensshModule):
self._remove()
def _should_generate(self):
if self.regenerate == 'never':
if self.regenerate == "never":
return self.original_data is None
elif self.regenerate == 'fail':
elif self.regenerate == "fail":
if self.original_data and not self._is_fully_valid():
self.module.fail_json(
msg="Certificate does not match the provided options.",
cert=get_cert_dict(self.original_data)
cert=get_cert_dict(self.original_data),
)
return self.original_data is None
elif self.regenerate == 'partial_idempotence':
elif self.regenerate == "partial_idempotence":
return self.original_data is None or not self._is_partially_valid()
elif self.regenerate == 'full_idempotence':
elif self.regenerate == "full_idempotence":
return self.original_data is None or not self._is_fully_valid()
else:
return True
def _is_fully_valid(self):
return self._is_partially_valid() and all([
self._compare_options() if self.original_data.type == 'user' else True,
self.original_data.key_id == self.identifier,
self.original_data.public_key == self._get_key_fingerprint(self.public_key),
self.original_data.signing_key == self._get_key_fingerprint(self.signing_key),
])
return self._is_partially_valid() and all(
[
self._compare_options() if self.original_data.type == "user" else True,
self.original_data.key_id == self.identifier,
self.original_data.public_key
== self._get_key_fingerprint(self.public_key),
self.original_data.signing_key
== self._get_key_fingerprint(self.signing_key),
]
)
def _is_partially_valid(self):
return all([
set(self.original_data.principals) == set(self.principals),
self.original_data.signature_type == self.signature_algorithm if self.signature_algorithm else True,
self.original_data.serial == self.serial_number if self.serial_number is not None else True,
self.original_data.type == self.type,
self._compare_time_parameters(),
])
return all(
[
set(self.original_data.principals) == set(self.principals),
(
self.original_data.signature_type == self.signature_algorithm
if self.signature_algorithm
else True
),
(
self.original_data.serial == self.serial_number
if self.serial_number is not None
else True
),
self.original_data.type == self.type,
self._compare_time_parameters(),
]
)
def _compare_time_parameters(self):
try:
original_time_parameters = OpensshCertificateTimeParameters(
valid_from=self.original_data.valid_after,
valid_to=self.original_data.valid_before
valid_to=self.original_data.valid_before,
)
except ValueError as e:
return self.module.fail_json(msg=to_native(e))
@@ -437,10 +459,12 @@ class Certificate(OpensshModule):
if self.ignore_timestamps:
return original_time_parameters.within_range(self.valid_at)
return all([
original_time_parameters == self.time_parameters,
original_time_parameters.within_range(self.valid_at)
])
return all(
[
original_time_parameters == self.time_parameters,
original_time_parameters.within_range(self.valid_at),
]
)
def _compare_options(self):
try:
@@ -448,10 +472,12 @@ class Certificate(OpensshModule):
except ValueError as e:
return self.module.fail_json(msg=to_native(e))
return all([
set(self.original_data.critical_options) == set(critical_options),
set(self.original_data.extensions) == set(extensions)
])
return all(
[
set(self.original_data.critical_options) == set(critical_options),
set(self.original_data.extensions) == set(extensions),
]
)
def _get_key_fingerprint(self, path):
private_key_content = self.ssh_keygen.get_private_key(path, check_rc=True)[1]
@@ -464,12 +490,16 @@ class Certificate(OpensshModule):
temp_certificate = self._generate_temp_certificate()
self._safe_secure_move([(temp_certificate, self.path)])
except OSError as e:
self.module.fail_json(msg="Unable to write certificate to %s: %s" % (self.path, to_native(e)))
self.module.fail_json(
msg="Unable to write certificate to %s: %s" % (self.path, to_native(e))
)
try:
self.data = OpensshCertificate.load(self.path)
except (TypeError, ValueError) as e:
self.module.fail_json(msg="Unable to read new certificate: %s" % to_native(e))
self.module.fail_json(
msg="Unable to read new certificate: %s" % to_native(e)
)
def _generate_temp_certificate(self):
key_copy = os.path.join(self.module.tmpdir, os.path.basename(self.public_key))
@@ -477,16 +507,28 @@ class Certificate(OpensshModule):
try:
self.module.preserved_copy(self.public_key, key_copy)
except OSError as e:
self.module.fail_json(msg="Unable to stage temporary key: %s" % to_native(e))
self.module.fail_json(
msg="Unable to stage temporary key: %s" % to_native(e)
)
self.module.add_cleanup_file(key_copy)
self.ssh_keygen.generate_certificate(
key_copy, self.identifier, self.options, self.pkcs11_provider, self.principals, self.serial_number,
self.signature_algorithm, self.signing_key, self.type, self.time_parameters, self.use_agent,
environ_update=dict(TZ="UTC"), check_rc=True
key_copy,
self.identifier,
self.options,
self.pkcs11_provider,
self.principals,
self.serial_number,
self.signature_algorithm,
self.signing_key,
self.type,
self.time_parameters,
self.use_agent,
environ_update=dict(TZ="UTC"),
check_rc=True,
)
temp_cert = os.path.splitext(key_copy)[0] + '-cert.pub'
temp_cert = os.path.splitext(key_copy)[0] + "-cert.pub"
self.module.add_cleanup_file(temp_cert)
return temp_cert
@@ -497,29 +539,31 @@ class Certificate(OpensshModule):
try:
os.remove(self.path)
except OSError as e:
self.module.fail_json(msg="Unable to remove existing certificate: %s" % to_native(e))
self.module.fail_json(
msg="Unable to remove existing certificate: %s" % to_native(e)
)
@property
def _result(self):
if self.state != 'present':
if self.state != "present":
return {}
certificate_info = self.ssh_keygen.get_certificate_info(
self.path,
check_rc=self.state == 'present' and not self.module.check_mode,
check_rc=self.state == "present" and not self.module.check_mode,
)[1]
return {
'type': self.type,
'filename': self.path,
'info': format_cert_info(certificate_info),
"type": self.type,
"filename": self.path,
"info": format_cert_info(certificate_info),
}
@property
def diff(self):
return {
'before': get_cert_dict(self.original_data),
'after': get_cert_dict(self.data)
"before": get_cert_dict(self.original_data),
"after": get_cert_dict(self.data),
}
@@ -528,7 +572,17 @@ def format_cert_info(cert_info):
string = ""
for word in cert_info.split():
if word in ("Type:", "Public", "Signing", "Key", "Serial:", "Valid:", "Principals:", "Critical", "Extensions:"):
if word in (
"Type:",
"Public",
"Signing",
"Key",
"Serial:",
"Valid:",
"Principals:",
"Critical",
"Extensions:",
):
result.append(string)
string = word
else:
@@ -544,8 +598,8 @@ def get_cert_dict(data):
return {}
result = data.to_dict()
result.pop('nonce')
result['signature_algorithm'] = data.signature_type
result.pop("nonce")
result["signature_algorithm"] = data.signature_type
return result
@@ -553,36 +607,50 @@ def get_cert_dict(data):
def main():
module = AnsibleModule(
argument_spec=dict(
force=dict(type='bool', default=False),
identifier=dict(type='str'),
options=dict(type='list', elements='str'),
path=dict(type='path', required=True),
pkcs11_provider=dict(type='str'),
principals=dict(type='list', elements='str'),
public_key=dict(type='path'),
force=dict(type="bool", default=False),
identifier=dict(type="str"),
options=dict(type="list", elements="str"),
path=dict(type="path", required=True),
pkcs11_provider=dict(type="str"),
principals=dict(type="list", elements="str"),
public_key=dict(type="path"),
regenerate=dict(
type='str',
default='partial_idempotence',
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
type="str",
default="partial_idempotence",
choices=[
"never",
"fail",
"partial_idempotence",
"full_idempotence",
"always",
],
),
signature_algorithm=dict(type='str', choices=['ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512']),
signing_key=dict(type='path'),
serial_number=dict(type='int'),
state=dict(type='str', default='present', choices=['absent', 'present']),
type=dict(type='str', choices=['host', 'user']),
use_agent=dict(type='bool', default=False),
valid_at=dict(type='str'),
valid_from=dict(type='str'),
valid_to=dict(type='str'),
ignore_timestamps=dict(type='bool', default=False),
signature_algorithm=dict(
type="str", choices=["ssh-rsa", "rsa-sha2-256", "rsa-sha2-512"]
),
signing_key=dict(type="path"),
serial_number=dict(type="int"),
state=dict(type="str", default="present", choices=["absent", "present"]),
type=dict(type="str", choices=["host", "user"]),
use_agent=dict(type="bool", default=False),
valid_at=dict(type="str"),
valid_from=dict(type="str"),
valid_to=dict(type="str"),
ignore_timestamps=dict(type="bool", default=False),
),
supports_check_mode=True,
add_file_common_args=True,
required_if=[('state', 'present', ['type', 'signing_key', 'public_key', 'valid_from', 'valid_to'])],
required_if=[
(
"state",
"present",
["type", "signing_key", "public_key", "valid_from", "valid_to"],
)
],
)
Certificate(module).execute()
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -213,33 +213,48 @@ def main():
module = AnsibleModule(
argument_spec=dict(
state=dict(type='str', default='present', choices=['present', 'absent']),
size=dict(type='int'),
type=dict(type='str', default='rsa', choices=['rsa', 'dsa', 'rsa1', 'ecdsa', 'ed25519']),
force=dict(type='bool', default=False),
path=dict(type='path', required=True),
comment=dict(type='str'),
regenerate=dict(
type='str',
default='partial_idempotence',
choices=['never', 'fail', 'partial_idempotence', 'full_idempotence', 'always']
state=dict(type="str", default="present", choices=["present", "absent"]),
size=dict(type="int"),
type=dict(
type="str",
default="rsa",
choices=["rsa", "dsa", "rsa1", "ecdsa", "ed25519"],
),
passphrase=dict(type='str', no_log=True),
force=dict(type="bool", default=False),
path=dict(type="path", required=True),
comment=dict(type="str"),
regenerate=dict(
type="str",
default="partial_idempotence",
choices=[
"never",
"fail",
"partial_idempotence",
"full_idempotence",
"always",
],
),
passphrase=dict(type="str", no_log=True),
private_key_format=dict(
type='str',
default='auto',
type="str",
default="auto",
no_log=False,
choices=['auto', 'pkcs1', 'pkcs8', 'ssh']),
backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'opensshbin'])
choices=["auto", "pkcs1", "pkcs8", "ssh"],
),
backend=dict(
type="str",
default="auto",
choices=["auto", "cryptography", "opensshbin"],
),
),
supports_check_mode=True,
add_file_common_args=True,
)
keypair = select_backend(module, module.params['backend'])[1]
keypair = select_backend(module, module.params["backend"])[1]
keypair.execute()
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -266,21 +266,21 @@ class CertificateSigningRequestModule(OpenSSLObject):
def __init__(self, module, module_backend):
super(CertificateSigningRequestModule, self).__init__(
module.params['path'],
module.params['state'],
module.params['force'],
module.check_mode
module.params["path"],
module.params["state"],
module.params["force"],
module.check_mode,
)
self.module_backend = module_backend
self.return_content = module.params['return_content']
self.return_content = module.params["return_content"]
self.backup = module.params['backup']
self.backup = module.params["backup"]
self.backup_file = None
self.module_backend.set_existing(load_file_if_exists(self.path, module))
def generate(self, module):
'''Generate the certificate signing request.'''
"""Generate the certificate signing request."""
if self.force or self.module_backend.needs_regeneration():
if not self.check_mode:
self.module_backend.generate_csr()
@@ -291,10 +291,12 @@ class CertificateSigningRequestModule(OpenSSLObject):
self.changed = True
file_args = module.load_file_common_arguments(module.params)
if module.check_file_absent_if_check_mode(file_args['path']):
if module.check_file_absent_if_check_mode(file_args["path"]):
self.changed = True
else:
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
self.changed = module.set_fs_attributes_if_different(
file_args, self.changed
)
def remove(self, module):
self.module_backend.set_existing(None)
@@ -303,43 +305,53 @@ class CertificateSigningRequestModule(OpenSSLObject):
super(CertificateSigningRequestModule, self).remove(module)
def dump(self):
'''Serialize the object into a dictionary.'''
"""Serialize the object into a dictionary."""
result = self.module_backend.dump(include_csr=self.return_content)
result.update({
'filename': self.path,
'changed': self.changed,
})
result.update(
{
"filename": self.path,
"changed": self.changed,
}
)
if self.backup_file:
result['backup_file'] = self.backup_file
result["backup_file"] = self.backup_file
return result
def main():
argument_spec = get_csr_argument_spec()
argument_spec.argument_spec.update(dict(
state=dict(type='str', default='present', choices=['absent', 'present']),
force=dict(type='bool', default=False),
path=dict(type='path', required=True),
backup=dict(type='bool', default=False),
return_content=dict(type='bool', default=False),
))
argument_spec.required_if.extend([('state', 'present', rof, True) for rof in argument_spec.required_one_of])
argument_spec.argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["absent", "present"]),
force=dict(type="bool", default=False),
path=dict(type="path", required=True),
backup=dict(type="bool", default=False),
return_content=dict(type="bool", default=False),
)
)
argument_spec.required_if.extend(
[("state", "present", rof, True) for rof in argument_spec.required_one_of]
)
argument_spec.required_one_of = []
module = argument_spec.create_ansible_module(
add_file_common_args=True,
supports_check_mode=True,
)
base_dir = os.path.dirname(module.params['path']) or '.'
base_dir = os.path.dirname(module.params["path"]) or "."
if not os.path.isdir(base_dir):
module.fail_json(name=base_dir, msg='The directory %s does not exist or the file is not a directory' % base_dir)
module.fail_json(
name=base_dir,
msg="The directory %s does not exist or the file is not a directory"
% base_dir,
)
try:
backend = module.params['select_crypto_backend']
backend = module.params["select_crypto_backend"]
backend, module_backend = select_backend(module, backend)
csr = CertificateSigningRequestModule(module, module_backend)
if module.params['state'] == 'present':
if module.params["state"] == "present":
csr.generate(module)
else:
csr.remove(module)

View File

@@ -325,30 +325,34 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
def main():
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path'),
content=dict(type='str'),
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
),
required_one_of=(
['path', 'content'],
),
mutually_exclusive=(
['path', 'content'],
path=dict(type="path"),
content=dict(type="str"),
name_encoding=dict(
type="str", default="ignore", choices=["ignore", "idna", "unicode"]
),
select_crypto_backend=dict(
type="str", default="auto", choices=["auto", "cryptography"]
),
),
required_one_of=(["path", "content"],),
mutually_exclusive=(["path", "content"],),
supports_check_mode=True,
)
if module.params['content'] is not None:
data = module.params['content'].encode('utf-8')
if module.params["content"] is not None:
data = module.params["content"].encode("utf-8")
else:
try:
with open(module.params['path'], 'rb') as f:
with open(module.params["path"], "rb") as f:
data = f.read()
except (IOError, OSError) as e:
module.fail_json(msg='Error while reading CSR file from disk: {0}'.format(e))
module.fail_json(
msg="Error while reading CSR file from disk: {0}".format(e)
)
backend, module_backend = select_backend(module, module.params['select_crypto_backend'], data, validate_signature=True)
backend, module_backend = select_backend(
module, module.params["select_crypto_backend"], data, validate_signature=True
)
try:
result = module_backend.get_info()

View File

@@ -151,46 +151,50 @@ class CertificateSigningRequestModule(object):
self.module = module
self.module_backend = module_backend
self.changed = False
if module.params['content'] is not None:
self.module_backend.set_existing(module.params['content'].encode('utf-8'))
if module.params["content"] is not None:
self.module_backend.set_existing(module.params["content"].encode("utf-8"))
def generate(self, module):
'''Generate the certificate signing request.'''
"""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',
"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.changed = True
def dump(self):
'''Serialize the object into a dictionary.'''
"""Serialize the object into a dictionary."""
result = self.module_backend.dump(include_csr=True)
result.update({
'changed': self.changed,
})
result.update(
{
"changed": self.changed,
}
)
return result
def main():
argument_spec = get_csr_argument_spec()
argument_spec.argument_spec.update(dict(
content=dict(type='str'),
))
argument_spec.argument_spec.update(
dict(
content=dict(type="str"),
)
)
module = argument_spec.create_ansible_module(
supports_check_mode=True,
)
try:
backend = module.params['select_crypto_backend']
backend = module.params["select_crypto_backend"]
backend, module_backend = select_backend(module, backend)
csr = CertificateSigningRequestModule(module, module_backend)

View File

@@ -153,7 +153,7 @@ from ansible_collections.community.crypto.plugins.module_utils.version import (
)
MINIMAL_CRYPTOGRAPHY_VERSION = '2.0'
MINIMAL_CRYPTOGRAPHY_VERSION = "2.0"
CRYPTOGRAPHY_IMP_ERR = None
try:
@@ -162,6 +162,7 @@ try:
import cryptography.hazmat.backends
import cryptography.hazmat.primitives.asymmetric.dh
import cryptography.hazmat.primitives.serialization
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -177,14 +178,14 @@ class DHParameterError(Exception):
class DHParameterBase(object):
def __init__(self, module):
self.state = module.params['state']
self.path = module.params['path']
self.size = module.params['size']
self.force = module.params['force']
self.state = module.params["state"]
self.path = module.params["path"]
self.size = module.params["size"]
self.force = module.params["force"]
self.changed = False
self.return_content = module.params['return_content']
self.return_content = module.params["return_content"]
self.backup = module.params['backup']
self.backup = module.params["backup"]
self.backup_file = None
@abc.abstractmethod
@@ -232,7 +233,7 @@ class DHParameterBase(object):
def _check_fs_attributes(self, module):
"""Checks (and changes if not in check mode!) fs attributes"""
file_args = module.load_file_common_arguments(module.params)
if module.check_file_absent_if_check_mode(file_args['path']):
if module.check_file_absent_if_check_mode(file_args["path"]):
return False
return not module.set_fs_attributes_if_different(file_args, False)
@@ -240,15 +241,15 @@ class DHParameterBase(object):
"""Serialize the object into a dictionary."""
result = {
'size': self.size,
'filename': self.path,
'changed': self.changed,
"size": self.size,
"filename": self.path,
"changed": self.changed,
}
if self.backup_file:
result['backup_file'] = self.backup_file
result["backup_file"] = self.backup_file
if self.return_content:
content = load_file_if_exists(self.path, ignore_errors=True)
result['dhparams'] = content.decode('utf-8') if content else None
result["dhparams"] = content.decode("utf-8") if content else None
return result
@@ -271,7 +272,7 @@ class DHParameterOpenSSL(DHParameterBase):
def __init__(self, module):
super(DHParameterOpenSSL, self).__init__(module)
self.openssl_bin = module.get_bin_path('openssl', True)
self.openssl_bin = module.get_bin_path("openssl", True)
def _do_generate(self, module):
"""Actually generate the DH params."""
@@ -280,7 +281,7 @@ class DHParameterOpenSSL(DHParameterBase):
os.close(fd)
module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
# openssl dhparam -out <path> <bits>
command = [self.openssl_bin, 'dhparam', '-out', tmpsrc, str(self.size)]
command = [self.openssl_bin, "dhparam", "-out", tmpsrc, str(self.size)]
rc, dummy, err = module.run_command(command, check_rc=False)
if rc != 0:
raise DHParameterError(to_native(err))
@@ -293,7 +294,15 @@ class DHParameterOpenSSL(DHParameterBase):
def _check_params_valid(self, module):
"""Check if the params are in the correct state"""
command = [self.openssl_bin, 'dhparam', '-check', '-text', '-noout', '-in', self.path]
command = [
self.openssl_bin,
"dhparam",
"-check",
"-text",
"-noout",
"-in",
self.path,
]
rc, out, err = module.run_command(command, check_rc=False)
result = to_native(out)
if rc != 0:
@@ -342,9 +351,11 @@ class DHParameterCryptography(DHParameterBase):
"""Check if the params are in the correct state"""
# Load parameters
try:
with open(self.path, 'rb') as f:
with open(self.path, "rb") as f:
data = f.read()
params = cryptography.hazmat.primitives.serialization.load_pem_parameters(data, backend=self.crypto_backend)
params = cryptography.hazmat.primitives.serialization.load_pem_parameters(
data, backend=self.crypto_backend
)
except Exception:
return False
# Check parameters
@@ -357,56 +368,70 @@ def main():
module = AnsibleModule(
argument_spec=dict(
state=dict(type='str', default='present', choices=['absent', 'present']),
size=dict(type='int', default=4096),
force=dict(type='bool', default=False),
path=dict(type='path', required=True),
backup=dict(type='bool', default=False),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography', 'openssl']),
return_content=dict(type='bool', default=False),
state=dict(type="str", default="present", choices=["absent", "present"]),
size=dict(type="int", default=4096),
force=dict(type="bool", default=False),
path=dict(type="path", required=True),
backup=dict(type="bool", default=False),
select_crypto_backend=dict(
type="str", default="auto", choices=["auto", "cryptography", "openssl"]
),
return_content=dict(type="bool", default=False),
),
supports_check_mode=True,
add_file_common_args=True,
)
base_dir = os.path.dirname(module.params['path']) or '.'
base_dir = os.path.dirname(module.params["path"]) or "."
if not os.path.isdir(base_dir):
module.fail_json(
name=base_dir,
msg="The directory '%s' does not exist or the file is not a directory" % base_dir
msg="The directory '%s' does not exist or the file is not a directory"
% base_dir,
)
if module.params['state'] == 'present':
backend = module.params['select_crypto_backend']
if backend == 'auto':
if module.params["state"] == "present":
backend = module.params["select_crypto_backend"]
if backend == "auto":
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_openssl = module.get_bin_path('openssl', False) is not None
can_use_cryptography = (
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
)
can_use_openssl = module.get_bin_path("openssl", False) is not None
# First try cryptography, then OpenSSL
if can_use_cryptography:
backend = 'cryptography'
backend = "cryptography"
elif can_use_openssl:
backend = 'openssl'
backend = "openssl"
# Success?
if backend == 'auto':
module.fail_json(msg=("Cannot detect either the required Python library cryptography (>= {0}) "
"or the OpenSSL binary openssl").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == "auto":
module.fail_json(
msg=(
"Cannot detect either the required Python library cryptography (>= {0}) "
"or the OpenSSL binary openssl"
).format(MINIMAL_CRYPTOGRAPHY_VERSION)
)
if backend == 'openssl':
if backend == "openssl":
dhparam = DHParameterOpenSSL(module)
elif backend == 'cryptography':
elif backend == "cryptography":
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
module.fail_json(
msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
dhparam = DHParameterCryptography(module)
else:
raise AssertionError('Internal error: unknown backend')
raise AssertionError("Internal error: unknown backend")
if module.check_mode:
result = dhparam.dump()
result['changed'] = module.params['force'] or not dhparam.check(module)
result["changed"] = module.params["force"] or not dhparam.check(module)
module.exit_json(**result)
try:
@@ -418,10 +443,10 @@ def main():
if module.check_mode:
result = dhparam.dump()
result['changed'] = os.path.exists(module.params['path'])
result["changed"] = os.path.exists(module.params["path"])
module.exit_json(**result)
if os.path.exists(module.params['path']):
if os.path.exists(module.params["path"]):
try:
dhparam.remove(module)
except Exception as exc:
@@ -432,5 +457,5 @@ def main():
module.exit_json(**result)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -314,9 +314,9 @@ 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'
MINIMAL_CRYPTOGRAPHY_VERSION = "3.0"
MINIMAL_PYOPENSSL_VERSION = "0.15"
MAXIMAL_PYOPENSSL_VERSION = "23.3.0"
PYOPENSSL_IMP_ERR = None
try:
@@ -325,6 +325,7 @@ try:
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()
@@ -339,6 +340,7 @@ try:
from cryptography.hazmat.primitives.serialization.pkcs12 import (
serialize_key_and_certificates,
)
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -352,7 +354,9 @@ try:
from cryptography.hazmat.primitives.serialization.pkcs12 import PBES
# Try to build encryption builder for compatibility2022
serialization.PrivateFormat.PKCS12.encryption_builder().key_cert_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC).hmac_hash(hashes.SHA1())
serialization.PrivateFormat.PKCS12.encryption_builder().key_cert_algorithm(
PBES.PBESv1SHA1And3KeyTripleDESCBC
).hmac_hash(hashes.SHA1())
except Exception:
CRYPTOGRAPHY_COMPATIBILITY2022_ERR = traceback.format_exc()
CRYPTOGRAPHY_HAS_COMPATIBILITY2022 = False
@@ -361,12 +365,15 @@ else:
def load_certificate_set(filename, backend):
'''
"""
Load list of concatenated PEM files, and return a list of parsed certificates.
'''
with open(filename, 'rb') as f:
data = f.read().decode('utf-8')
return [load_certificate(None, content=cert.encode('utf-8'), backend=backend) for cert in split_pem_list(data)]
"""
with open(filename, "rb") as f:
data = f.read().decode("utf-8")
return [
load_certificate(None, content=cert.encode("utf-8"), backend=backend)
for cert in split_pem_list(data)
]
class PkcsError(OpenSSLObjectError):
@@ -376,40 +383,42 @@ class PkcsError(OpenSSLObjectError):
class Pkcs(OpenSSLObject):
def __init__(self, module, backend, iter_size_default=2048):
super(Pkcs, self).__init__(
module.params['path'],
module.params['state'],
module.params['force'],
module.check_mode
module.params["path"],
module.params["state"],
module.params["force"],
module.check_mode,
)
self.backend = backend
self.action = module.params['action']
self.other_certificates = module.params['other_certificates']
self.other_certificates_parse_all = module.params['other_certificates_parse_all']
self.other_certificates_content = module.params['other_certificates_content']
self.certificate_path = module.params['certificate_path']
self.certificate_content = module.params['certificate_content']
self.friendly_name = module.params['friendly_name']
self.iter_size = module.params['iter_size'] or iter_size_default
self.maciter_size = module.params['maciter_size'] or 1
self.encryption_level = module.params['encryption_level']
self.passphrase = module.params['passphrase']
self.action = module.params["action"]
self.other_certificates = module.params["other_certificates"]
self.other_certificates_parse_all = module.params[
"other_certificates_parse_all"
]
self.other_certificates_content = module.params["other_certificates_content"]
self.certificate_path = module.params["certificate_path"]
self.certificate_content = module.params["certificate_content"]
self.friendly_name = module.params["friendly_name"]
self.iter_size = module.params["iter_size"] or iter_size_default
self.maciter_size = module.params["maciter_size"] or 1
self.encryption_level = module.params["encryption_level"]
self.passphrase = module.params["passphrase"]
self.pkcs12 = None
self.privatekey_passphrase = module.params['privatekey_passphrase']
self.privatekey_path = module.params['privatekey_path']
self.privatekey_content = module.params['privatekey_content']
self.privatekey_passphrase = module.params["privatekey_passphrase"]
self.privatekey_path = module.params["privatekey_path"]
self.privatekey_content = module.params["privatekey_content"]
self.pkcs12_bytes = None
self.return_content = module.params['return_content']
self.src = module.params['src']
self.return_content = module.params["return_content"]
self.src = module.params["src"]
if module.params['mode'] is None:
module.params['mode'] = '0400'
if module.params["mode"] is None:
module.params["mode"] = "0400"
self.backup = module.params['backup']
self.backup = module.params["backup"]
self.backup_file = None
if self.certificate_path is not None:
try:
with open(self.certificate_path, 'rb') as fh:
with open(self.certificate_path, "rb") as fh:
self.certificate_content = fh.read()
except (IOError, OSError) as exc:
raise PkcsError(exc)
@@ -418,7 +427,7 @@ class Pkcs(OpenSSLObject):
if self.privatekey_path is not None:
try:
with open(self.privatekey_path, 'rb') as fh:
with open(self.privatekey_path, "rb") as fh:
self.privatekey_content = fh.read()
except (IOError, OSError) as exc:
raise PkcsError(exc)
@@ -430,17 +439,27 @@ class Pkcs(OpenSSLObject):
filenames = list(self.other_certificates)
self.other_certificates = []
for other_cert_bundle in filenames:
self.other_certificates.extend(load_certificate_set(other_cert_bundle, self.backend))
self.other_certificates.extend(
load_certificate_set(other_cert_bundle, self.backend)
)
else:
self.other_certificates = [
load_certificate(other_cert, backend=self.backend) for other_cert in self.other_certificates
load_certificate(other_cert, backend=self.backend)
for other_cert in self.other_certificates
]
elif self.other_certificates_content:
certs = self.other_certificates_content
if self.other_certificates_parse_all:
certs = list(itertools.chain.from_iterable(split_pem_list(content) for content in certs))
certs = list(
itertools.chain.from_iterable(
split_pem_list(content) for content in certs
)
)
self.other_certificates = [
load_certificate(None, content=to_bytes(other_cert), backend=self.backend) for other_cert in certs
load_certificate(
None, content=to_bytes(other_cert), backend=self.backend
)
for other_cert in certs
]
@abc.abstractmethod
@@ -476,7 +495,12 @@ class Pkcs(OpenSSLObject):
def _check_pkey_passphrase():
if self.privatekey_passphrase:
try:
load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend)
load_privatekey(
None,
content=self.privatekey_content,
passphrase=self.privatekey_passphrase,
backend=self.backend,
)
except OpenSSLObjectError:
return False
return True
@@ -484,28 +508,39 @@ class Pkcs(OpenSSLObject):
if not state_and_perms:
return state_and_perms
if os.path.exists(self.path) and module.params['action'] == 'export':
if os.path.exists(self.path) and module.params["action"] == "export":
self.generate_bytes(module) # ignore result
self.src = self.path
try:
pkcs12_privatekey, pkcs12_certificate, pkcs12_other_certificates, pkcs12_friendly_name = self.parse()
(
pkcs12_privatekey,
pkcs12_certificate,
pkcs12_other_certificates,
pkcs12_friendly_name,
) = self.parse()
except OpenSSLObjectError:
return False
if (pkcs12_privatekey is not None) and (self.privatekey_content is not None):
if (pkcs12_privatekey is not None) and (
self.privatekey_content is not None
):
expected_pkey = self._dump_privatekey(self.pkcs12)
if pkcs12_privatekey != expected_pkey:
return False
elif bool(pkcs12_privatekey) != bool(self.privatekey_content):
return False
if (pkcs12_certificate is not None) and (self.certificate_content is not None):
if (pkcs12_certificate is not None) and (
self.certificate_content is not None
):
expected_cert = self._dump_certificate(self.pkcs12)
if pkcs12_certificate != expected_cert:
return False
elif bool(pkcs12_certificate) != bool(self.certificate_content):
return False
if (pkcs12_other_certificates is not None) and (self.other_certificates is not None):
if (pkcs12_other_certificates is not None) and (
self.other_certificates is not None
):
expected_other_certs = self._dump_other_certificates(self.pkcs12)
if set(pkcs12_other_certificates) != set(expected_other_certs):
return False
@@ -516,18 +551,28 @@ class Pkcs(OpenSSLObject):
# This check is required because pyOpenSSL will not return a friendly name
# if the private key is not set in the file
friendly_name = self._get_friendly_name(self.pkcs12)
if ((friendly_name is not None) and (pkcs12_friendly_name is not None)):
if (friendly_name is not None) and (pkcs12_friendly_name is not None):
if friendly_name != pkcs12_friendly_name:
return False
elif bool(friendly_name) != bool(pkcs12_friendly_name):
return False
elif module.params['action'] == 'parse' and os.path.exists(self.src) and os.path.exists(self.path):
elif (
module.params["action"] == "parse"
and os.path.exists(self.src)
and os.path.exists(self.path)
):
try:
pkey, cert, other_certs, friendly_name = self.parse()
except OpenSSLObjectError:
return False
expected_content = to_bytes(
''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None])
"".join(
[
to_native(pem)
for pem in [pkey, cert] + other_certs
if pem is not None
]
)
)
dumped_content = load_file_if_exists(self.path, ignore_errors=True)
if expected_content != dumped_content:
@@ -541,16 +586,18 @@ class Pkcs(OpenSSLObject):
"""Serialize the object into a dictionary."""
result = {
'filename': self.path,
"filename": self.path,
}
if self.privatekey_path:
result['privatekey_path'] = self.privatekey_path
result["privatekey_path"] = self.privatekey_path
if self.backup_file:
result['backup_file'] = self.backup_file
result["backup_file"] = self.backup_file
if self.return_content:
if self.pkcs12_bytes is None:
self.pkcs12_bytes = load_file_if_exists(self.path, ignore_errors=True)
result['pkcs12'] = base64.b64encode(self.pkcs12_bytes) if self.pkcs12_bytes else None
result["pkcs12"] = (
base64.b64encode(self.pkcs12_bytes) if self.pkcs12_bytes else None
)
return result
@@ -563,7 +610,7 @@ class Pkcs(OpenSSLObject):
"""Read PKCS#12 file."""
try:
with open(self.src, 'rb') as pkcs12_fh:
with open(self.src, "rb") as pkcs12_fh:
pkcs12_content = pkcs12_fh.read()
return self.parse_bytes(pkcs12_content)
except IOError as exc:
@@ -583,9 +630,11 @@ class Pkcs(OpenSSLObject):
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')
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."""
@@ -595,7 +644,11 @@ class PkcsPyOpenSSL(Pkcs):
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))
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))
@@ -603,7 +656,13 @@ class PkcsPyOpenSSL(Pkcs):
if self.privatekey_content:
try:
self.pkcs12.set_privatekey(
load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend))
load_privatekey(
None,
content=self.privatekey_content,
passphrase=self.privatekey_passphrase,
backend=self.backend,
)
)
except OpenSSLBadPassphraseError as exc:
raise PkcsError(exc)
@@ -620,8 +679,10 @@ class PkcsPyOpenSSL(Pkcs):
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()]
other_certs = [
crypto.dump_certificate(crypto.FILETYPE_PEM, other_cert)
for other_cert in p12.get_ca_certificates()
]
friendly_name = p12.get_friendlyname()
@@ -651,43 +712,60 @@ class PkcsPyOpenSSL(Pkcs):
class PkcsCryptography(Pkcs):
def __init__(self, module):
super(PkcsCryptography, self).__init__(module, 'cryptography', iter_size_default=50000)
if self.encryption_level == 'compatibility2022' and not CRYPTOGRAPHY_HAS_COMPATIBILITY2022:
super(PkcsCryptography, self).__init__(
module, "cryptography", iter_size_default=50000
)
if (
self.encryption_level == "compatibility2022"
and not CRYPTOGRAPHY_HAS_COMPATIBILITY2022
):
module.fail_json(
msg='The installed cryptography version does not support encryption_level = compatibility2022.'
' You need cryptography >= 38.0.0 and support for SHA1',
exception=CRYPTOGRAPHY_COMPATIBILITY2022_ERR)
msg="The installed cryptography version does not support encryption_level = compatibility2022."
" You need cryptography >= 38.0.0 and support for SHA1",
exception=CRYPTOGRAPHY_COMPATIBILITY2022_ERR,
)
def generate_bytes(self, module):
"""Generate PKCS#12 file archive."""
pkey = None
if self.privatekey_content:
try:
pkey = load_privatekey(None, content=self.privatekey_content, passphrase=self.privatekey_passphrase, backend=self.backend)
pkey = load_privatekey(
None,
content=self.privatekey_content,
passphrase=self.privatekey_passphrase,
backend=self.backend,
)
except OpenSSLBadPassphraseError as exc:
raise PkcsError(exc)
cert = None
if self.certificate_content:
cert = load_certificate(None, content=self.certificate_content, backend=self.backend)
cert = load_certificate(
None, content=self.certificate_content, backend=self.backend
)
friendly_name = to_bytes(self.friendly_name) if self.friendly_name is not None else None
friendly_name = (
to_bytes(self.friendly_name) if self.friendly_name is not None else None
)
# Store fake object which can be used to retrieve the components back
self.pkcs12 = (pkey, cert, self.other_certificates, friendly_name)
if not self.passphrase:
encryption = serialization.NoEncryption()
elif self.encryption_level == 'compatibility2022':
elif self.encryption_level == "compatibility2022":
encryption = (
serialization.PrivateFormat.PKCS12.encryption_builder().
kdf_rounds(self.iter_size).
key_cert_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC).
hmac_hash(hashes.SHA1()).
build(to_bytes(self.passphrase))
serialization.PrivateFormat.PKCS12.encryption_builder()
.kdf_rounds(self.iter_size)
.key_cert_algorithm(PBES.PBESv1SHA1And3KeyTripleDESCBC)
.hmac_hash(hashes.SHA1())
.build(to_bytes(self.passphrase))
)
else:
encryption = serialization.BestAvailableEncryption(to_bytes(self.passphrase))
encryption = serialization.BestAvailableEncryption(
to_bytes(self.passphrase)
)
return serialize_key_and_certificates(
friendly_name,
@@ -699,8 +777,9 @@ class PkcsCryptography(Pkcs):
def parse_bytes(self, pkcs12_content):
try:
private_key, certificate, additional_certificates, friendly_name = parse_pkcs12(
pkcs12_content, self.passphrase)
private_key, certificate, additional_certificates, friendly_name = (
parse_pkcs12(pkcs12_content, self.passphrase)
)
pkey = None
if private_key is not None:
@@ -730,103 +809,133 @@ class PkcsCryptography(Pkcs):
# self.pkcs12 = (pkey, cert, self.other_certificates, self.friendly_name)
def _dump_privatekey(self, pkcs12):
return pkcs12[0].private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
) if pkcs12[0] else None
return (
pkcs12[0].private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
if pkcs12[0]
else None
)
def _dump_certificate(self, pkcs12):
return pkcs12[1].public_bytes(serialization.Encoding.PEM) if pkcs12[1] else None
def _dump_other_certificates(self, pkcs12):
return [other_cert.public_bytes(serialization.Encoding.PEM) for other_cert in pkcs12[2]]
return [
other_cert.public_bytes(serialization.Encoding.PEM)
for other_cert in pkcs12[2]
]
def _get_friendly_name(self, pkcs12):
return pkcs12[3]
def select_backend(module, backend):
if backend == 'auto':
if backend == "auto":
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_cryptography = (
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)
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
):
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'
backend = "pyopenssl"
elif can_use_cryptography:
backend = 'cryptography'
backend = "cryptography"
elif can_use_pyopenssl:
backend = '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})").format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION,
MAXIMAL_PYOPENSSL_VERSION))
if backend == "auto":
module.fail_json(
msg=(
"Cannot detect any of the required Python libraries "
"cryptography (>= {0}) or PyOpenSSL (>= {1}, < {2})"
).format(
MINIMAL_CRYPTOGRAPHY_VERSION,
MINIMAL_PYOPENSSL_VERSION,
MAXIMAL_PYOPENSSL_VERSION,
)
)
if backend == 'pyopenssl':
if backend == "pyopenssl":
if not PYOPENSSL_FOUND:
msg = missing_required_lib(
'pyOpenSSL >= {0}, < {1}'.format(MINIMAL_PYOPENSSL_VERSION, MAXIMAL_PYOPENSSL_VERSION)
"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')
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':
elif backend == "cryptography":
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
module.fail_json(
msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
return backend, PkcsCryptography(module)
else:
raise ValueError('Unsupported value for backend: {0}'.format(backend))
raise ValueError("Unsupported value for backend: {0}".format(backend))
def main():
argument_spec = dict(
action=dict(type='str', default='export', choices=['export', 'parse']),
other_certificates=dict(type='list', elements='path', aliases=['ca_certificates']),
other_certificates_parse_all=dict(type='bool', default=False),
other_certificates_content=dict(type='list', elements='str'),
certificate_path=dict(type='path'),
certificate_content=dict(type='str'),
force=dict(type='bool', default=False),
friendly_name=dict(type='str', aliases=['name']),
encryption_level=dict(type='str', choices=['auto', 'compatibility2022'], default='auto'),
iter_size=dict(type='int'),
maciter_size=dict(type='int'),
passphrase=dict(type='str', no_log=True),
path=dict(type='path', required=True),
privatekey_passphrase=dict(type='str', no_log=True),
privatekey_path=dict(type='path'),
privatekey_content=dict(type='str', no_log=True),
state=dict(type='str', default='present', choices=['absent', 'present']),
src=dict(type='path'),
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']),
action=dict(type="str", default="export", choices=["export", "parse"]),
other_certificates=dict(
type="list", elements="path", aliases=["ca_certificates"]
),
other_certificates_parse_all=dict(type="bool", default=False),
other_certificates_content=dict(type="list", elements="str"),
certificate_path=dict(type="path"),
certificate_content=dict(type="str"),
force=dict(type="bool", default=False),
friendly_name=dict(type="str", aliases=["name"]),
encryption_level=dict(
type="str", choices=["auto", "compatibility2022"], default="auto"
),
iter_size=dict(type="int"),
maciter_size=dict(type="int"),
passphrase=dict(type="str", no_log=True),
path=dict(type="path", required=True),
privatekey_passphrase=dict(type="str", no_log=True),
privatekey_path=dict(type="path"),
privatekey_content=dict(type="str", no_log=True),
state=dict(type="str", default="present", choices=["absent", "present"]),
src=dict(type="path"),
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"]
),
)
required_if = [
['action', 'parse', ['src']],
["action", "parse", ["src"]],
]
mutually_exclusive = [
['privatekey_path', 'privatekey_content'],
['certificate_path', 'certificate_content'],
['other_certificates', 'other_certificates_content'],
["privatekey_path", "privatekey_content"],
["certificate_path", "certificate_content"],
["other_certificates", "other_certificates_content"],
]
module = AnsibleModule(
@@ -837,62 +946,69 @@ def main():
supports_check_mode=True,
)
backend, pkcs12 = select_backend(module, module.params['select_crypto_backend'])
backend, pkcs12 = select_backend(module, module.params["select_crypto_backend"])
base_dir = os.path.dirname(module.params['path']) or '.'
base_dir = os.path.dirname(module.params["path"]) or "."
if not os.path.isdir(base_dir):
module.fail_json(
name=base_dir,
msg="The directory '%s' does not exist or the path is not a directory" % base_dir
msg="The directory '%s' does not exist or the path is not a directory"
% base_dir,
)
try:
changed = False
if module.params['state'] == 'present':
if module.params["state"] == "present":
if module.check_mode:
result = pkcs12.dump()
result['changed'] = module.params['force'] or not pkcs12.check(module)
result["changed"] = module.params["force"] or not pkcs12.check(module)
module.exit_json(**result)
if not pkcs12.check(module, perms_required=False) or module.params['force']:
if module.params['action'] == 'export':
if not module.params['friendly_name']:
module.fail_json(msg='Friendly_name is required')
if not pkcs12.check(module, perms_required=False) or module.params["force"]:
if module.params["action"] == "export":
if not module.params["friendly_name"]:
module.fail_json(msg="Friendly_name is required")
pkcs12_content = pkcs12.generate_bytes(module)
pkcs12.write(module, pkcs12_content, 0o600)
changed = True
else:
pkey, cert, other_certs, friendly_name = pkcs12.parse()
dump_content = ''.join([to_native(pem) for pem in [pkey, cert] + other_certs if pem is not None])
dump_content = "".join(
[
to_native(pem)
for pem in [pkey, cert] + other_certs
if pem is not None
]
)
pkcs12.write(module, to_bytes(dump_content))
changed = True
file_args = module.load_file_common_arguments(module.params)
if module.check_file_absent_if_check_mode(file_args['path']):
if module.check_file_absent_if_check_mode(file_args["path"]):
changed = True
elif module.set_fs_attributes_if_different(file_args, changed):
changed = True
else:
if module.check_mode:
result = pkcs12.dump()
result['changed'] = os.path.exists(module.params['path'])
result["changed"] = os.path.exists(module.params["path"])
module.exit_json(**result)
if os.path.exists(module.params['path']):
if os.path.exists(module.params["path"]):
pkcs12.remove(module)
changed = True
result = pkcs12.dump()
result['changed'] = changed
if os.path.exists(module.params['path']):
file_mode = "%04o" % stat.S_IMODE(os.stat(module.params['path']).st_mode)
result['mode'] = file_mode
result["changed"] = changed
if os.path.exists(module.params["path"]):
file_mode = "%04o" % stat.S_IMODE(os.stat(module.params["path"]).st_mode)
result["mode"] = file_mode
module.exit_json(**result)
except OpenSSLObjectError as exc:
module.fail_json(msg=to_native(exc))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -182,21 +182,21 @@ class PrivateKeyModule(OpenSSLObject):
def __init__(self, module, module_backend):
super(PrivateKeyModule, self).__init__(
module.params['path'],
module.params['state'],
module.params['force'],
module.params["path"],
module.params["state"],
module.params["force"],
module.check_mode,
)
self.module_backend = module_backend
self.return_content = module.params['return_content']
self.return_content = module.params["return_content"]
if self.force:
module_backend.regenerate = 'always'
module_backend.regenerate = "always"
self.backup = module.params['backup']
self.backup = module.params["backup"]
self.backup_file = None
if module.params['mode'] is None:
module.params['mode'] = '0600'
if module.params["mode"] is None:
module.params["mode"] = "0600"
module_backend.set_existing(load_file_if_exists(self.path, module))
@@ -227,10 +227,12 @@ class PrivateKeyModule(OpenSSLObject):
self.changed = True
file_args = module.load_file_common_arguments(module.params)
if module.check_file_absent_if_check_mode(file_args['path']):
if module.check_file_absent_if_check_mode(file_args["path"]):
self.changed = True
else:
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
self.changed = module.set_fs_attributes_if_different(
file_args, self.changed
)
def remove(self, module):
self.module_backend.set_existing(None)
@@ -242,10 +244,10 @@ class PrivateKeyModule(OpenSSLObject):
"""Serialize the object into a dictionary."""
result = self.module_backend.dump(include_key=self.return_content)
result['filename'] = self.path
result['changed'] = self.changed
result["filename"] = self.path
result["changed"] = self.changed
if self.backup_file:
result['backup_file'] = self.backup_file
result["backup_file"] = self.backup_file
return result
@@ -253,34 +255,37 @@ class PrivateKeyModule(OpenSSLObject):
def main():
argument_spec = get_privatekey_argument_spec()
argument_spec.argument_spec.update(dict(
state=dict(type='str', default='present', choices=['present', 'absent']),
force=dict(type='bool', default=False),
path=dict(type='path', required=True),
backup=dict(type='bool', default=False),
return_content=dict(type='bool', default=False),
))
argument_spec.argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["present", "absent"]),
force=dict(type="bool", default=False),
path=dict(type="path", required=True),
backup=dict(type="bool", default=False),
return_content=dict(type="bool", default=False),
)
)
module = argument_spec.create_ansible_module(
supports_check_mode=True,
add_file_common_args=True,
)
base_dir = os.path.dirname(module.params['path']) or '.'
base_dir = os.path.dirname(module.params["path"]) or "."
if not os.path.isdir(base_dir):
module.fail_json(
name=base_dir,
msg='The directory %s does not exist or the file is not a directory' % base_dir
msg="The directory %s does not exist or the file is not a directory"
% base_dir,
)
backend, module_backend = select_backend(
module=module,
backend=module.params['select_crypto_backend'],
backend=module.params["select_crypto_backend"],
)
try:
private_key = PrivateKeyModule(module, module_backend)
if private_key.state == 'present':
if private_key.state == "present":
private_key.generate(module)
else:
private_key.remove(module)
@@ -291,5 +296,5 @@ def main():
module.fail_json(msg=to_native(exc))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -86,19 +86,19 @@ from ansible_collections.community.crypto.plugins.module_utils.io import (
class PrivateKeyConvertModule(OpenSSLObject):
def __init__(self, module, module_backend):
super(PrivateKeyConvertModule, self).__init__(
module.params['dest_path'],
'present',
module.params["dest_path"],
"present",
False,
module.check_mode,
)
self.module_backend = module_backend
self.backup = module.params['backup']
self.backup = module.params["backup"]
self.backup_file = None
module.params['path'] = module.params['dest_path']
if module.params['mode'] is None:
module.params['mode'] = '0600'
module.params["path"] = module.params["dest_path"]
if module.params["mode"] is None:
module.params["mode"] = "0600"
module_backend.set_existing_destination(load_file_if_exists(self.path, module))
@@ -115,18 +115,20 @@ class PrivateKeyConvertModule(OpenSSLObject):
self.changed = True
file_args = module.load_file_common_arguments(module.params)
if module.check_file_absent_if_check_mode(file_args['path']):
if module.check_file_absent_if_check_mode(file_args["path"]):
self.changed = True
else:
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
self.changed = module.set_fs_attributes_if_different(
file_args, self.changed
)
def dump(self):
"""Serialize the object into a dictionary."""
result = self.module_backend.dump()
result['changed'] = self.changed
result["changed"] = self.changed
if self.backup_file:
result['backup_file'] = self.backup_file
result["backup_file"] = self.backup_file
return result
@@ -134,20 +136,23 @@ class PrivateKeyConvertModule(OpenSSLObject):
def main():
argument_spec = get_privatekey_argument_spec()
argument_spec.argument_spec.update(dict(
dest_path=dict(type='path', required=True),
backup=dict(type='bool', default=False),
))
argument_spec.argument_spec.update(
dict(
dest_path=dict(type="path", required=True),
backup=dict(type="bool", default=False),
)
)
module = argument_spec.create_ansible_module(
supports_check_mode=True,
add_file_common_args=True,
)
base_dir = os.path.dirname(module.params['dest_path']) or '.'
base_dir = os.path.dirname(module.params["dest_path"]) or "."
if not os.path.isdir(base_dir):
module.fail_json(
name=base_dir,
msg='The directory %s does not exist or the file is not a directory' % base_dir
msg="The directory %s does not exist or the file is not a directory"
% base_dir,
)
module_backend = select_backend(module=module)
@@ -163,5 +168,5 @@ def main():
module.fail_json(msg=to_native(exc))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -219,19 +219,17 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
def main():
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path'),
content=dict(type='str', no_log=True),
passphrase=dict(type='str', no_log=True),
return_private_key_data=dict(type='bool', default=False),
check_consistency=dict(type='bool', default=False),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
),
required_one_of=(
['path', 'content'],
),
mutually_exclusive=(
['path', 'content'],
path=dict(type="path"),
content=dict(type="str", no_log=True),
passphrase=dict(type="str", no_log=True),
return_private_key_data=dict(type="bool", default=False),
check_consistency=dict(type="bool", default=False),
select_crypto_backend=dict(
type="str", default="auto", choices=["auto", "cryptography"]
),
),
required_one_of=(["path", "content"],),
mutually_exclusive=(["path", "content"],),
supports_check_mode=True,
)
@@ -241,24 +239,28 @@ def main():
key_is_consistent=None,
)
if module.params['content'] is not None:
data = module.params['content'].encode('utf-8')
if module.params["content"] is not None:
data = module.params["content"].encode("utf-8")
else:
try:
with open(module.params['path'], 'rb') as f:
with open(module.params["path"], "rb") as f:
data = f.read()
except (IOError, OSError) as e:
module.fail_json(msg='Error while reading private key file from disk: {0}'.format(e), **result)
module.fail_json(
msg="Error while reading private key file from disk: {0}".format(e),
**result
)
result['can_load_key'] = True
result["can_load_key"] = True
backend, module_backend = select_backend(
module,
module.params['select_crypto_backend'],
module.params["select_crypto_backend"],
data,
passphrase=module.params['passphrase'],
return_private_key_data=module.params['return_private_key_data'],
check_consistency=module.params['check_consistency'])
passphrase=module.params["passphrase"],
return_private_key_data=module.params["return_private_key_data"],
check_consistency=module.params["check_consistency"],
)
try:
result.update(module_backend.get_info())

View File

@@ -216,14 +216,15 @@ from ansible_collections.community.crypto.plugins.module_utils.version import (
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2.3'
MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = '1.4'
MINIMAL_CRYPTOGRAPHY_VERSION = "1.2.3"
MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH = "1.4"
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization as crypto_serialization
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -240,25 +241,25 @@ class PublicKey(OpenSSLObject):
def __init__(self, module, backend):
super(PublicKey, self).__init__(
module.params['path'],
module.params['state'],
module.params['force'],
module.check_mode
module.params["path"],
module.params["state"],
module.params["force"],
module.check_mode,
)
self.module = module
self.format = module.params['format']
self.privatekey_path = module.params['privatekey_path']
self.privatekey_content = module.params['privatekey_content']
self.format = module.params["format"]
self.privatekey_path = module.params["privatekey_path"]
self.privatekey_content = module.params["privatekey_content"]
if self.privatekey_content is not None:
self.privatekey_content = self.privatekey_content.encode('utf-8')
self.privatekey_passphrase = module.params['privatekey_passphrase']
self.privatekey_content = self.privatekey_content.encode("utf-8")
self.privatekey_passphrase = module.params["privatekey_passphrase"]
self.privatekey = None
self.publickey_bytes = None
self.return_content = module.params['return_content']
self.return_content = module.params["return_content"]
self.fingerprint = {}
self.backend = backend
self.backup = module.params['backup']
self.backup = module.params["backup"]
self.backup_file = None
self.diff_before = self._get_info(None)
@@ -269,9 +270,12 @@ class PublicKey(OpenSSLObject):
return dict()
result = dict(can_parse_key=False)
try:
result.update(get_publickey_info(
self.module, self.backend, content=data, prefer_one_fingerprint=True))
result['can_parse_key'] = True
result.update(
get_publickey_info(
self.module, self.backend, content=data, prefer_one_fingerprint=True
)
)
result["can_parse_key"] = True
except PublicKeyParseError as exc:
result.update(exc.result)
except Exception:
@@ -283,18 +287,18 @@ class PublicKey(OpenSSLObject):
path=self.privatekey_path,
content=self.privatekey_content,
passphrase=self.privatekey_passphrase,
backend=self.backend
backend=self.backend,
)
if self.backend == 'cryptography':
if self.format == 'OpenSSH':
if self.backend == "cryptography":
if self.format == "OpenSSH":
return self.privatekey.public_key().public_bytes(
crypto_serialization.Encoding.OpenSSH,
crypto_serialization.PublicFormat.OpenSSH
crypto_serialization.PublicFormat.OpenSSH,
)
else:
return self.privatekey.public_key().public_bytes(
crypto_serialization.Encoding.PEM,
crypto_serialization.PublicFormat.SubjectPublicKeyInfo
crypto_serialization.PublicFormat.SubjectPublicKeyInfo,
)
def generate(self, module):
@@ -302,7 +306,7 @@ class PublicKey(OpenSSLObject):
if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
raise PublicKeyError(
'The private key %s does not exist' % self.privatekey_path
"The private key %s does not exist" % self.privatekey_path
)
if not self.check(module, perms_required=False) or self.force:
@@ -329,7 +333,7 @@ class PublicKey(OpenSSLObject):
backend=self.backend,
)
file_args = module.load_file_common_arguments(module.params)
if module.check_file_absent_if_check_mode(file_args['path']):
if module.check_file_absent_if_check_mode(file_args["path"]):
self.changed = True
elif module.set_fs_attributes_if_different(file_args, False):
self.changed = True
@@ -340,28 +344,34 @@ class PublicKey(OpenSSLObject):
state_and_perms = super(PublicKey, self).check(module, perms_required)
def _check_privatekey():
if self.privatekey_content is None and not os.path.exists(self.privatekey_path):
if self.privatekey_content is None and not os.path.exists(
self.privatekey_path
):
return False
try:
with open(self.path, 'rb') as public_key_fh:
with open(self.path, "rb") as public_key_fh:
publickey_content = public_key_fh.read()
self.diff_before = self.diff_after = self._get_info(publickey_content)
if self.return_content:
self.publickey_bytes = publickey_content
if self.backend == 'cryptography':
if self.format == 'OpenSSH':
if self.backend == "cryptography":
if self.format == "OpenSSH":
# Read and dump public key. Makes sure that the comment is stripped off.
current_publickey = crypto_serialization.load_ssh_public_key(publickey_content, backend=default_backend())
current_publickey = crypto_serialization.load_ssh_public_key(
publickey_content, backend=default_backend()
)
publickey_content = current_publickey.public_bytes(
crypto_serialization.Encoding.OpenSSH,
crypto_serialization.PublicFormat.OpenSSH
crypto_serialization.PublicFormat.OpenSSH,
)
else:
current_publickey = crypto_serialization.load_pem_public_key(publickey_content, backend=default_backend())
current_publickey = crypto_serialization.load_pem_public_key(
publickey_content, backend=default_backend()
)
publickey_content = current_publickey.public_bytes(
crypto_serialization.Encoding.PEM,
crypto_serialization.PublicFormat.SubjectPublicKeyInfo
crypto_serialization.PublicFormat.SubjectPublicKeyInfo,
)
except Exception:
return False
@@ -387,20 +397,24 @@ class PublicKey(OpenSSLObject):
"""Serialize the object into a dictionary."""
result = {
'privatekey': self.privatekey_path,
'filename': self.path,
'format': self.format,
'changed': self.changed,
'fingerprint': self.fingerprint,
"privatekey": self.privatekey_path,
"filename": self.path,
"format": self.format,
"changed": self.changed,
"fingerprint": self.fingerprint,
}
if self.backup_file:
result['backup_file'] = self.backup_file
result["backup_file"] = self.backup_file
if self.return_content:
if self.publickey_bytes is None:
self.publickey_bytes = load_file_if_exists(self.path, ignore_errors=True)
result['publickey'] = self.publickey_bytes.decode('utf-8') if self.publickey_bytes else None
self.publickey_bytes = load_file_if_exists(
self.path, ignore_errors=True
)
result["publickey"] = (
self.publickey_bytes.decode("utf-8") if self.publickey_bytes else None
)
result['diff'] = dict(
result["diff"] = dict(
before=self.diff_before,
after=self.diff_after,
)
@@ -412,72 +426,87 @@ def main():
module = AnsibleModule(
argument_spec=dict(
state=dict(type='str', default='present', choices=['present', 'absent']),
force=dict(type='bool', default=False),
path=dict(type='path', required=True),
privatekey_path=dict(type='path'),
privatekey_content=dict(type='str', no_log=True),
format=dict(type='str', default='PEM', choices=['OpenSSH', 'PEM']),
privatekey_passphrase=dict(type='str', no_log=True),
backup=dict(type='bool', default=False),
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
return_content=dict(type='bool', default=False),
state=dict(type="str", default="present", choices=["present", "absent"]),
force=dict(type="bool", default=False),
path=dict(type="path", required=True),
privatekey_path=dict(type="path"),
privatekey_content=dict(type="str", no_log=True),
format=dict(type="str", default="PEM", choices=["OpenSSH", "PEM"]),
privatekey_passphrase=dict(type="str", no_log=True),
backup=dict(type="bool", default=False),
select_crypto_backend=dict(
type="str", choices=["auto", "cryptography"], default="auto"
),
return_content=dict(type="bool", default=False),
),
supports_check_mode=True,
add_file_common_args=True,
required_if=[('state', 'present', ['privatekey_path', 'privatekey_content'], True)],
mutually_exclusive=(
['privatekey_path', 'privatekey_content'],
),
required_if=[
("state", "present", ["privatekey_path", "privatekey_content"], True)
],
mutually_exclusive=(["privatekey_path", "privatekey_content"],),
)
minimal_cryptography_version = MINIMAL_CRYPTOGRAPHY_VERSION
if module.params['format'] == 'OpenSSH':
if module.params["format"] == "OpenSSH":
minimal_cryptography_version = MINIMAL_CRYPTOGRAPHY_VERSION_OPENSSH
backend = module.params['select_crypto_backend']
if backend == 'auto':
backend = module.params["select_crypto_backend"]
if backend == "auto":
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(minimal_cryptography_version)
can_use_cryptography = (
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(minimal_cryptography_version)
)
# Decision
if can_use_cryptography:
backend = 'cryptography'
backend = "cryptography"
# Success?
if backend == 'auto':
module.fail_json(msg=("Cannot detect the required Python library "
"cryptography (>= {0})").format(minimal_cryptography_version))
if backend == "auto":
module.fail_json(
msg=(
"Cannot detect the required Python library " "cryptography (>= {0})"
).format(minimal_cryptography_version)
)
if module.params['format'] == 'OpenSSH' and backend != 'cryptography':
if module.params["format"] == "OpenSSH" and backend != "cryptography":
module.fail_json(msg="Format OpenSSH requires the cryptography backend.")
if backend == 'cryptography':
if backend == "cryptography":
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(minimal_cryptography_version)),
exception=CRYPTOGRAPHY_IMP_ERR)
module.fail_json(
msg=missing_required_lib(
"cryptography >= {0}".format(minimal_cryptography_version)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
base_dir = os.path.dirname(module.params['path']) or '.'
base_dir = os.path.dirname(module.params["path"]) or "."
if not os.path.isdir(base_dir):
module.fail_json(
name=base_dir,
msg="The directory '%s' does not exist or the file is not a directory" % base_dir
msg="The directory '%s' does not exist or the file is not a directory"
% base_dir,
)
try:
public_key = PublicKey(module, backend)
if public_key.state == 'present':
if public_key.state == "present":
if module.check_mode:
result = public_key.dump()
result['changed'] = module.params['force'] or not public_key.check(module)
result["changed"] = module.params["force"] or not public_key.check(
module
)
module.exit_json(**result)
public_key.generate(module)
else:
if module.check_mode:
result = public_key.dump()
result['changed'] = os.path.exists(module.params['path'])
result["changed"] = os.path.exists(module.params["path"])
module.exit_json(**result)
public_key.remove(module)
@@ -488,5 +517,5 @@ def main():
module.fail_json(msg=to_native(exc))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -170,16 +170,14 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
def main():
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path'),
content=dict(type='str', no_log=True),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
),
required_one_of=(
['path', 'content'],
),
mutually_exclusive=(
['path', 'content'],
path=dict(type="path"),
content=dict(type="str", no_log=True),
select_crypto_backend=dict(
type="str", default="auto", choices=["auto", "cryptography"]
),
),
required_one_of=(["path", "content"],),
mutually_exclusive=(["path", "content"],),
supports_check_mode=True,
)
@@ -189,19 +187,21 @@ def main():
key_is_consistent=None,
)
if module.params['content'] is not None:
data = module.params['content'].encode('utf-8')
if module.params["content"] is not None:
data = module.params["content"].encode("utf-8")
else:
try:
with open(module.params['path'], 'rb') as f:
with open(module.params["path"], "rb") as f:
data = f.read()
except (IOError, OSError) as e:
module.fail_json(msg='Error while reading public key file from disk: {0}'.format(e), **result)
module.fail_json(
msg="Error while reading public key file from disk: {0}".format(e),
**result
)
backend, module_backend = select_backend(
module,
module.params['select_crypto_backend'],
data)
module, module.params["select_crypto_backend"], data
)
try:
result.update(module_backend.get_info())

View File

@@ -113,13 +113,14 @@ from ansible_collections.community.crypto.plugins.module_utils.version import (
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
MINIMAL_CRYPTOGRAPHY_VERSION = "1.4"
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
import cryptography.hazmat.primitives.asymmetric.padding
import cryptography.hazmat.primitives.hashes
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -147,19 +148,19 @@ class SignatureBase(OpenSSLObject):
def __init__(self, module, backend):
super(SignatureBase, self).__init__(
path=module.params['path'],
state='present',
path=module.params["path"],
state="present",
force=False,
check_mode=module.check_mode
check_mode=module.check_mode,
)
self.backend = backend
self.privatekey_path = module.params['privatekey_path']
self.privatekey_content = module.params['privatekey_content']
self.privatekey_path = module.params["privatekey_path"]
self.privatekey_content = module.params["privatekey_content"]
if self.privatekey_content is not None:
self.privatekey_content = self.privatekey_content.encode('utf-8')
self.privatekey_passphrase = module.params['privatekey_passphrase']
self.privatekey_content = self.privatekey_content.encode("utf-8")
self.privatekey_passphrase = module.params["privatekey_passphrase"]
def generate(self):
# Empty method because OpenSSLObject wants this
@@ -196,31 +197,50 @@ class SignatureCryptography(SignatureBase):
signature = None
if CRYPTOGRAPHY_HAS_DSA_SIGN:
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey):
if isinstance(
private_key,
cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey,
):
signature = private_key.sign(_in, _hash)
if CRYPTOGRAPHY_HAS_EC_SIGN:
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey):
signature = private_key.sign(_in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash))
if isinstance(
private_key,
cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey,
):
signature = private_key.sign(
_in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash)
)
if CRYPTOGRAPHY_HAS_ED25519_SIGN:
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey):
if isinstance(
private_key,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey,
):
signature = private_key.sign(_in)
if CRYPTOGRAPHY_HAS_ED448_SIGN:
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey):
if isinstance(
private_key,
cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey,
):
signature = private_key.sign(_in)
if CRYPTOGRAPHY_HAS_RSA_SIGN:
if isinstance(private_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey):
if isinstance(
private_key,
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey,
):
signature = private_key.sign(_in, _padding, _hash)
if signature is None:
self.module.fail_json(
msg="Unsupported key type. Your cryptography version is {0}".format(CRYPTOGRAPHY_VERSION)
msg="Unsupported key type. Your cryptography version is {0}".format(
CRYPTOGRAPHY_VERSION
)
)
result['signature'] = base64.b64encode(signature)
result["signature"] = base64.b64encode(signature)
return result
except Exception as e:
@@ -230,45 +250,53 @@ class SignatureCryptography(SignatureBase):
def main():
module = AnsibleModule(
argument_spec=dict(
privatekey_path=dict(type='path'),
privatekey_content=dict(type='str', no_log=True),
privatekey_passphrase=dict(type='str', no_log=True),
path=dict(type='path', required=True),
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
),
mutually_exclusive=(
['privatekey_path', 'privatekey_content'],
),
required_one_of=(
['privatekey_path', 'privatekey_content'],
privatekey_path=dict(type="path"),
privatekey_content=dict(type="str", no_log=True),
privatekey_passphrase=dict(type="str", no_log=True),
path=dict(type="path", required=True),
select_crypto_backend=dict(
type="str", choices=["auto", "cryptography"], default="auto"
),
),
mutually_exclusive=(["privatekey_path", "privatekey_content"],),
required_one_of=(["privatekey_path", "privatekey_content"],),
supports_check_mode=True,
)
if not os.path.isfile(module.params['path']):
if not os.path.isfile(module.params["path"]):
module.fail_json(
name=module.params['path'],
msg='The file {0} does not exist'.format(module.params['path'])
name=module.params["path"],
msg="The file {0} does not exist".format(module.params["path"]),
)
backend = module.params['select_crypto_backend']
if backend == 'auto':
backend = module.params["select_crypto_backend"]
if backend == "auto":
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_cryptography = (
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
)
# Decision
if can_use_cryptography:
backend = 'cryptography'
backend = "cryptography"
# Success?
if backend == 'auto':
module.fail_json(msg=("Cannot detect the required Python library "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == "auto":
module.fail_json(
msg=(
"Cannot detect the required Python library " "cryptography (>= {0})"
).format(MINIMAL_CRYPTOGRAPHY_VERSION)
)
try:
if backend == 'cryptography':
if backend == "cryptography":
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
module.fail_json(
msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
_sign = SignatureCryptography(module, backend)
result = _sign.run()
@@ -278,5 +306,5 @@ def main():
module.fail_json(msg=to_native(exc))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -102,13 +102,14 @@ from ansible_collections.community.crypto.plugins.module_utils.version import (
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.4'
MINIMAL_CRYPTOGRAPHY_VERSION = "1.4"
CRYPTOGRAPHY_IMP_ERR = None
try:
import cryptography
import cryptography.hazmat.primitives.asymmetric.padding
import cryptography.hazmat.primitives.hashes
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -136,19 +137,19 @@ class SignatureInfoBase(OpenSSLObject):
def __init__(self, module, backend):
super(SignatureInfoBase, self).__init__(
path=module.params['path'],
state='present',
path=module.params["path"],
state="present",
force=False,
check_mode=module.check_mode
check_mode=module.check_mode,
)
self.backend = backend
self.signature = module.params['signature']
self.certificate_path = module.params['certificate_path']
self.certificate_content = module.params['certificate_content']
self.signature = module.params["signature"]
self.certificate_path = module.params["certificate_path"]
self.certificate_content = module.params["certificate_content"]
if self.certificate_content is not None:
self.certificate_content = self.certificate_content.encode('utf-8')
self.certificate_content = self.certificate_content.encode("utf-8")
def generate(self):
# Empty method because OpenSSLObject wants this
@@ -187,7 +188,10 @@ class SignatureInfoCryptography(SignatureInfoBase):
if CRYPTOGRAPHY_HAS_DSA_SIGN:
try:
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey):
if isinstance(
public_key,
cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey,
):
public_key.verify(_signature, _in, _hash)
verified = True
valid = True
@@ -197,8 +201,15 @@ class SignatureInfoCryptography(SignatureInfoBase):
if CRYPTOGRAPHY_HAS_EC_SIGN:
try:
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey):
public_key.verify(_signature, _in, cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash))
if isinstance(
public_key,
cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey,
):
public_key.verify(
_signature,
_in,
cryptography.hazmat.primitives.asymmetric.ec.ECDSA(_hash),
)
verified = True
valid = True
except cryptography.exceptions.InvalidSignature:
@@ -207,7 +218,10 @@ class SignatureInfoCryptography(SignatureInfoBase):
if CRYPTOGRAPHY_HAS_ED25519_SIGN:
try:
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey):
if isinstance(
public_key,
cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey,
):
public_key.verify(_signature, _in)
verified = True
valid = True
@@ -217,7 +231,10 @@ class SignatureInfoCryptography(SignatureInfoBase):
if CRYPTOGRAPHY_HAS_ED448_SIGN:
try:
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey):
if isinstance(
public_key,
cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey,
):
public_key.verify(_signature, _in)
verified = True
valid = True
@@ -227,7 +244,10 @@ class SignatureInfoCryptography(SignatureInfoBase):
if CRYPTOGRAPHY_HAS_RSA_SIGN:
try:
if isinstance(public_key, cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey):
if isinstance(
public_key,
cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey,
):
public_key.verify(_signature, _in, _padding, _hash)
verified = True
valid = True
@@ -237,9 +257,11 @@ class SignatureInfoCryptography(SignatureInfoBase):
if not verified:
self.module.fail_json(
msg="Unsupported key type. Your cryptography version is {0}".format(CRYPTOGRAPHY_VERSION)
msg="Unsupported key type. Your cryptography version is {0}".format(
CRYPTOGRAPHY_VERSION
)
)
result['valid'] = valid
result["valid"] = valid
return result
except Exception as e:
@@ -249,45 +271,54 @@ class SignatureInfoCryptography(SignatureInfoBase):
def main():
module = AnsibleModule(
argument_spec=dict(
certificate_path=dict(type='path'),
certificate_content=dict(type='str'),
path=dict(type='path', required=True),
signature=dict(type='str', required=True),
select_crypto_backend=dict(type='str', choices=['auto', 'cryptography'], default='auto'),
),
mutually_exclusive=(
['certificate_path', 'certificate_content'],
),
required_one_of=(
['certificate_path', 'certificate_content'],
certificate_path=dict(type="path"),
certificate_content=dict(type="str"),
path=dict(type="path", required=True),
signature=dict(type="str", required=True),
select_crypto_backend=dict(
type="str", choices=["auto", "cryptography"], default="auto"
),
),
mutually_exclusive=(["certificate_path", "certificate_content"],),
required_one_of=(["certificate_path", "certificate_content"],),
supports_check_mode=True,
)
if not os.path.isfile(module.params['path']):
if not os.path.isfile(module.params["path"]):
module.fail_json(
name=module.params['path'],
msg='The file {0} does not exist'.format(module.params['path'])
name=module.params["path"],
msg="The file {0} does not exist".format(module.params["path"]),
)
backend = module.params['select_crypto_backend']
if backend == 'auto':
backend = module.params["select_crypto_backend"]
if backend == "auto":
# Detection what is possible
can_use_cryptography = CRYPTOGRAPHY_FOUND and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
can_use_cryptography = (
CRYPTOGRAPHY_FOUND
and CRYPTOGRAPHY_VERSION >= LooseVersion(MINIMAL_CRYPTOGRAPHY_VERSION)
)
# Decision
if can_use_cryptography:
backend = 'cryptography'
backend = "cryptography"
# Success?
if backend == 'auto':
module.fail_json(msg=("Cannot detect any of the required Python libraries "
"cryptography (>= {0})").format(MINIMAL_CRYPTOGRAPHY_VERSION))
if backend == "auto":
module.fail_json(
msg=(
"Cannot detect any of the required Python libraries "
"cryptography (>= {0})"
).format(MINIMAL_CRYPTOGRAPHY_VERSION)
)
try:
if backend == 'cryptography':
if backend == "cryptography":
if not CRYPTOGRAPHY_FOUND:
module.fail_json(msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
module.fail_json(
msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
_sign = SignatureInfoCryptography(module, backend)
result = _sign.run()
@@ -297,5 +328,5 @@ def main():
module.fail_json(msg=to_native(exc))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -266,14 +266,14 @@ from ansible_collections.community.crypto.plugins.module_utils.io import (
class CertificateAbsent(OpenSSLObject):
def __init__(self, module):
super(CertificateAbsent, self).__init__(
module.params['path'],
module.params['state'],
module.params['force'],
module.check_mode
module.params["path"],
module.params["state"],
module.params["force"],
module.check_mode,
)
self.module = module
self.return_content = module.params['return_content']
self.backup = module.params['backup']
self.return_content = module.params["return_content"]
self.backup = module.params["backup"]
self.backup_file = None
def generate(self, module):
@@ -286,31 +286,32 @@ class CertificateAbsent(OpenSSLObject):
def dump(self, check_mode=False):
result = {
'changed': self.changed,
'filename': self.path,
'privatekey': self.module.params['privatekey_path'],
'csr': self.module.params['csr_path']
"changed": self.changed,
"filename": self.path,
"privatekey": self.module.params["privatekey_path"],
"csr": self.module.params["csr_path"],
}
if self.backup_file:
result['backup_file'] = self.backup_file
result["backup_file"] = self.backup_file
if self.return_content:
result['certificate'] = None
result["certificate"] = None
return result
class GenericCertificate(OpenSSLObject):
"""Retrieve a certificate using the given module backend."""
def __init__(self, module, module_backend):
super(GenericCertificate, self).__init__(
module.params['path'],
module.params['state'],
module.params['force'],
module.check_mode
module.params["path"],
module.params["state"],
module.params["force"],
module.check_mode,
)
self.module = module
self.return_content = module.params['return_content']
self.backup = module.params['backup']
self.return_content = module.params["return_content"]
self.backup = module.params["backup"]
self.backup_file = None
self.module_backend = module_backend
@@ -327,23 +328,30 @@ class GenericCertificate(OpenSSLObject):
self.changed = True
file_args = module.load_file_common_arguments(module.params)
if module.check_file_absent_if_check_mode(file_args['path']):
if module.check_file_absent_if_check_mode(file_args["path"]):
self.changed = True
else:
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
self.changed = module.set_fs_attributes_if_different(
file_args, self.changed
)
def check(self, module, perms_required=True):
"""Ensure the resource is in its desired state."""
return super(GenericCertificate, self).check(module, perms_required) and not self.module_backend.needs_regeneration()
return (
super(GenericCertificate, self).check(module, perms_required)
and not self.module_backend.needs_regeneration()
)
def dump(self, check_mode=False):
result = self.module_backend.dump(include_certificate=self.return_content)
result.update({
'changed': self.changed,
'filename': self.path,
})
result.update(
{
"changed": self.changed,
"filename": self.path,
}
)
if self.backup_file:
result['backup_file'] = self.backup_file
result["backup_file"] = self.backup_file
return result
@@ -353,46 +361,49 @@ def main():
add_entrust_provider_to_argument_spec(argument_spec)
add_ownca_provider_to_argument_spec(argument_spec)
add_selfsigned_provider_to_argument_spec(argument_spec)
argument_spec.argument_spec.update(dict(
state=dict(type='str', default='present', choices=['present', 'absent']),
path=dict(type='path', required=True),
backup=dict(type='bool', default=False),
return_content=dict(type='bool', default=False),
))
argument_spec.required_if.append(['state', 'present', ['provider']])
argument_spec.argument_spec.update(
dict(
state=dict(type="str", default="present", choices=["present", "absent"]),
path=dict(type="path", required=True),
backup=dict(type="bool", default=False),
return_content=dict(type="bool", default=False),
)
)
argument_spec.required_if.append(["state", "present", ["provider"]])
module = argument_spec.create_ansible_module(
add_file_common_args=True,
supports_check_mode=True,
)
try:
if module.params['state'] == 'absent':
if module.params["state"] == "absent":
certificate = CertificateAbsent(module)
if module.check_mode:
result = certificate.dump(check_mode=True)
result['changed'] = os.path.exists(module.params['path'])
result["changed"] = os.path.exists(module.params["path"])
module.exit_json(**result)
certificate.remove(module)
else:
base_dir = os.path.dirname(module.params['path']) or '.'
base_dir = os.path.dirname(module.params["path"]) or "."
if not os.path.isdir(base_dir):
module.fail_json(
name=base_dir,
msg='The directory %s does not exist or the file is not a directory' % base_dir
msg="The directory %s does not exist or the file is not a directory"
% base_dir,
)
provider = module.params['provider']
provider = module.params["provider"]
provider_map = {
'acme': AcmeCertificateProvider,
'entrust': EntrustCertificateProvider,
'ownca': OwnCACertificateProvider,
'selfsigned': SelfSignedCertificateProvider,
"acme": AcmeCertificateProvider,
"entrust": EntrustCertificateProvider,
"ownca": OwnCACertificateProvider,
"selfsigned": SelfSignedCertificateProvider,
}
backend = module.params['select_crypto_backend']
backend = module.params["select_crypto_backend"]
module_backend = select_backend(module, backend, provider_map[provider]())
certificate = GenericCertificate(module, module_backend)
certificate.generate(module)

View File

@@ -137,7 +137,7 @@ from ansible_collections.community.crypto.plugins.module_utils.io import (
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.6'
MINIMAL_CRYPTOGRAPHY_VERSION = "1.6"
CRYPTOGRAPHY_IMP_ERR = None
try:
@@ -152,14 +152,22 @@ else:
def parse_certificate(input, strict=False):
input_format = 'pem' if identify_pem_format(input) else 'der'
if input_format == 'pem':
input_format = "pem" if identify_pem_format(input) else "der"
if input_format == "pem":
pems = split_pem_list(to_text(input))
if len(pems) > 1 and strict:
raise ValueError('The input contains {count} PEM objects, expecting only one since strict=true'.format(count=len(pems)))
raise ValueError(
"The input contains {count} PEM objects, expecting only one since strict=true".format(
count=len(pems)
)
)
pem_header_type, content = extract_pem(pems[0], strict=strict)
if strict and pem_header_type not in ('CERTIFICATE', 'X509 CERTIFICATE'):
raise ValueError('type is {type!r}, expecting CERTIFICATE or X509 CERTIFICATE'.format(type=pem_header_type))
if strict and pem_header_type not in ("CERTIFICATE", "X509 CERTIFICATE"):
raise ValueError(
"type is {type!r}, expecting CERTIFICATE or X509 CERTIFICATE".format(
type=pem_header_type
)
)
input = base64.b64decode(content)
else:
pem_header_type = None
@@ -169,66 +177,81 @@ def parse_certificate(input, strict=False):
class X509CertificateConvertModule(OpenSSLObject):
def __init__(self, module):
super(X509CertificateConvertModule, self).__init__(
module.params['dest_path'],
'present',
module.params["dest_path"],
"present",
False,
module.check_mode,
)
self.src_path = module.params['src_path']
self.src_content = module.params['src_content']
self.src_content_base64 = module.params['src_content_base64']
self.src_path = module.params["src_path"]
self.src_content = module.params["src_content"]
self.src_content_base64 = module.params["src_content_base64"]
if self.src_content is not None:
self.input = to_bytes(self.src_content)
if self.src_content_base64:
try:
self.input = base64.b64decode(self.input)
except Exception as exc:
module.fail_json(msg='Cannot Base64 decode src_content: {exc}'.format(exc=exc))
module.fail_json(
msg="Cannot Base64 decode src_content: {exc}".format(exc=exc)
)
else:
try:
with open(self.src_path, 'rb') as f:
with open(self.src_path, "rb") as f:
self.input = f.read()
except Exception as exc:
module.fail_json(msg='Failure while reading file {fn}: {exc}'.format(fn=self.src_path, exc=exc))
module.fail_json(
msg="Failure while reading file {fn}: {exc}".format(
fn=self.src_path, exc=exc
)
)
self.format = module.params['format']
self.strict = module.params['strict']
self.wanted_pem_type = 'CERTIFICATE'
self.format = module.params["format"]
self.strict = module.params["strict"]
self.wanted_pem_type = "CERTIFICATE"
try:
self.input, self.input_format, dummy = parse_certificate(self.input, strict=self.strict)
self.input, self.input_format, dummy = parse_certificate(
self.input, strict=self.strict
)
except Exception as exc:
module.fail_json(msg='Error while parsing PEM: {exc}'.format(exc=exc))
module.fail_json(msg="Error while parsing PEM: {exc}".format(exc=exc))
if module.params['verify_cert_parsable']:
if module.params["verify_cert_parsable"]:
self.verify_cert_parsable(module)
self.backup = module.params['backup']
self.backup = module.params["backup"]
self.backup_file = None
module.params['path'] = self.path
module.params["path"] = self.path
self.dest_content = load_file_if_exists(self.path, module)
self.dest_content_format = None
self.dest_content_pem_type = None
if self.dest_content is not None:
try:
self.dest_content, self.dest_content_format, self.dest_content_pem_type = parse_certificate(
self.dest_content, strict=True)
(
self.dest_content,
self.dest_content_format,
self.dest_content_pem_type,
) = parse_certificate(self.dest_content, strict=True)
except Exception:
pass
def verify_cert_parsable(self, module):
if not CRYPTOGRAPHY_FOUND:
module.fail_json(
msg=missing_required_lib('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
try:
load_der_x509_certificate(self.input, default_backend())
except Exception as exc:
module.fail_json(msg='Error while parsing certificate: {exc}'.format(exc=exc))
module.fail_json(
msg="Error while parsing certificate: {exc}".format(exc=exc)
)
def needs_conversion(self):
if self.dest_content is None or self.dest_content_format is None:
@@ -237,18 +260,20 @@ class X509CertificateConvertModule(OpenSSLObject):
return True
if self.input != self.dest_content:
return True
if self.format == 'pem' and self.dest_content_pem_type != self.wanted_pem_type:
if self.format == "pem" and self.dest_content_pem_type != self.wanted_pem_type:
return True
return False
def get_dest_certificate(self):
if self.format == 'der':
if self.format == "der":
return self.input
data = to_bytes(base64.b64encode(self.input))
lines = [to_bytes('{0}{1}{2}'.format(PEM_START, self.wanted_pem_type, PEM_END))]
lines += [data[i:i + 64] for i in range(0, len(data), 64)]
lines.append(to_bytes('{0}{1}{2}\n'.format(PEM_END_START, self.wanted_pem_type, PEM_END)))
return b'\n'.join(lines)
lines = [to_bytes("{0}{1}{2}".format(PEM_START, self.wanted_pem_type, PEM_END))]
lines += [data[i : i + 64] for i in range(0, len(data), 64)]
lines.append(
to_bytes("{0}{1}{2}\n".format(PEM_END_START, self.wanted_pem_type, PEM_END))
)
return b"\n".join(lines)
def generate(self, module):
"""Do conversion."""
@@ -262,10 +287,12 @@ class X509CertificateConvertModule(OpenSSLObject):
self.changed = True
file_args = module.load_file_common_arguments(module.params)
if module.check_file_absent_if_check_mode(file_args['path']):
if module.check_file_absent_if_check_mode(file_args["path"]):
self.changed = True
else:
self.changed = module.set_fs_attributes_if_different(file_args, self.changed)
self.changed = module.set_fs_attributes_if_different(
file_args, self.changed
)
def dump(self):
"""Serialize the object into a dictionary."""
@@ -273,35 +300,36 @@ class X509CertificateConvertModule(OpenSSLObject):
changed=self.changed,
)
if self.backup_file:
result['backup_file'] = self.backup_file
result["backup_file"] = self.backup_file
return result
def main():
argument_spec = dict(
src_path=dict(type='path'),
src_content=dict(type='str'),
src_content_base64=dict(type='bool', default=False),
format=dict(type='str', required=True, choices=['pem', 'der']),
strict=dict(type='bool', default=False),
dest_path=dict(type='path', required=True),
backup=dict(type='bool', default=False),
verify_cert_parsable=dict(type='bool', default=False),
src_path=dict(type="path"),
src_content=dict(type="str"),
src_content_base64=dict(type="bool", default=False),
format=dict(type="str", required=True, choices=["pem", "der"]),
strict=dict(type="bool", default=False),
dest_path=dict(type="path", required=True),
backup=dict(type="bool", default=False),
verify_cert_parsable=dict(type="bool", default=False),
)
module = AnsibleModule(
argument_spec,
supports_check_mode=True,
add_file_common_args=True,
required_one_of=[('src_path', 'src_content')],
mutually_exclusive=[('src_path', 'src_content')],
required_one_of=[("src_path", "src_content")],
mutually_exclusive=[("src_path", "src_content")],
)
base_dir = os.path.dirname(module.params['dest_path']) or '.'
base_dir = os.path.dirname(module.params["dest_path"]) or "."
if not os.path.isdir(base_dir):
module.fail_json(
name=base_dir,
msg='The directory %s does not exist or the file is not a directory' % base_dir
msg="The directory %s does not exist or the file is not a directory"
% base_dir,
)
try:
@@ -313,5 +341,5 @@ def main():
module.fail_json(msg=to_native(exc))
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -414,51 +414,61 @@ from ansible_collections.community.crypto.plugins.module_utils.time import (
def main():
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path'),
content=dict(type='str'),
valid_at=dict(type='dict'),
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
select_crypto_backend=dict(type='str', default='auto', choices=['auto', 'cryptography']),
),
required_one_of=(
['path', 'content'],
),
mutually_exclusive=(
['path', 'content'],
path=dict(type="path"),
content=dict(type="str"),
valid_at=dict(type="dict"),
name_encoding=dict(
type="str", default="ignore", choices=["ignore", "idna", "unicode"]
),
select_crypto_backend=dict(
type="str", default="auto", choices=["auto", "cryptography"]
),
),
required_one_of=(["path", "content"],),
mutually_exclusive=(["path", "content"],),
supports_check_mode=True,
)
if module.params['content'] is not None:
data = module.params['content'].encode('utf-8')
if module.params["content"] is not None:
data = module.params["content"].encode("utf-8")
else:
try:
with open(module.params['path'], 'rb') as f:
with open(module.params["path"], "rb") as f:
data = f.read()
except (IOError, OSError) as e:
module.fail_json(msg='Error while reading certificate file from disk: {0}'.format(e))
module.fail_json(
msg="Error while reading certificate file from disk: {0}".format(e)
)
backend, module_backend = select_backend(module, module.params['select_crypto_backend'], data)
backend, module_backend = select_backend(
module, module.params["select_crypto_backend"], data
)
valid_at = module.params['valid_at']
valid_at = module.params["valid_at"]
if valid_at:
for k, v in valid_at.items():
if not isinstance(v, string_types):
module.fail_json(
msg='The value for valid_at.{0} must be of type string (got {1})'.format(k, type(v))
msg="The value for valid_at.{0} must be of type string (got {1})".format(
k, type(v)
)
)
valid_at[k] = get_relative_time_option(v, 'valid_at.{0}'.format(k), with_timezone=CRYPTOGRAPHY_TIMEZONE)
valid_at[k] = get_relative_time_option(
v, "valid_at.{0}".format(k), with_timezone=CRYPTOGRAPHY_TIMEZONE
)
try:
result = module_backend.get_info(der_support_enabled=module.params['content'] is None)
result = module_backend.get_info(
der_support_enabled=module.params["content"] is None
)
not_before = module_backend.get_not_before()
not_after = module_backend.get_not_after()
result['valid_at'] = dict()
result["valid_at"] = dict()
if valid_at:
for k, v in valid_at.items():
result['valid_at'][k] = not_before <= v <= not_after
result["valid_at"][k] = not_before <= v <= not_after
module.exit_json(**result)
except OpenSSLObjectError as exc:

View File

@@ -151,13 +151,14 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.module_bac
class GenericCertificate(object):
"""Retrieve a certificate using the given module backend."""
def __init__(self, module, module_backend):
self.check_mode = module.check_mode
self.module = module
self.module_backend = module_backend
self.changed = False
if module.params['content'] is not None:
self.module_backend.set_existing(module.params['content'].encode('utf-8'))
if module.params["content"] is not None:
self.module_backend.set_existing(module.params["content"].encode("utf-8"))
def generate(self, module):
if self.module_backend.needs_regeneration():
@@ -165,46 +166,50 @@ class GenericCertificate(object):
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',
"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.changed = True
def dump(self, check_mode=False):
result = self.module_backend.dump(include_certificate=True)
result.update({
'changed': self.changed,
})
result.update(
{
"changed": self.changed,
}
)
return result
def main():
argument_spec = get_certificate_argument_spec()
argument_spec.argument_spec['provider']['required'] = True
argument_spec.argument_spec["provider"]["required"] = True
add_entrust_provider_to_argument_spec(argument_spec)
add_ownca_provider_to_argument_spec(argument_spec)
add_selfsigned_provider_to_argument_spec(argument_spec)
argument_spec.argument_spec.update(dict(
content=dict(type='str'),
))
argument_spec.argument_spec.update(
dict(
content=dict(type="str"),
)
)
module = argument_spec.create_ansible_module(
supports_check_mode=True,
)
try:
provider = module.params['provider']
provider = module.params["provider"]
provider_map = {
'entrust': EntrustCertificateProvider,
'ownca': OwnCACertificateProvider,
'selfsigned': SelfSignedCertificateProvider,
"entrust": EntrustCertificateProvider,
"ownca": OwnCACertificateProvider,
"selfsigned": SelfSignedCertificateProvider,
}
backend = module.params['select_crypto_backend']
backend = module.params["select_crypto_backend"]
module_backend = select_backend(module, backend, provider_map[provider]())
certificate = GenericCertificate(module, module_backend)
certificate.generate(module)

View File

@@ -496,7 +496,7 @@ from ansible_collections.community.crypto.plugins.module_utils.version import (
)
MINIMAL_CRYPTOGRAPHY_VERSION = '1.2'
MINIMAL_CRYPTOGRAPHY_VERSION = "1.2"
CRYPTOGRAPHY_IMP_ERR = None
try:
@@ -510,6 +510,7 @@ try:
NameAttribute,
RevokedCertificateBuilder,
)
CRYPTOGRAPHY_VERSION = LooseVersion(cryptography.__version__)
except ImportError:
CRYPTOGRAPHY_IMP_ERR = traceback.format_exc()
@@ -526,100 +527,122 @@ class CRL(OpenSSLObject):
def __init__(self, module):
super(CRL, self).__init__(
module.params['path'],
module.params['state'],
module.params['force'],
module.check_mode
module.params["path"],
module.params["state"],
module.params["force"],
module.check_mode,
)
self.format = module.params['format']
self.format = module.params["format"]
self.update = module.params['crl_mode'] == 'update'
self.ignore_timestamps = module.params['ignore_timestamps']
self.return_content = module.params['return_content']
self.name_encoding = module.params['name_encoding']
self.serial_numbers_format = module.params['serial_numbers']
self.update = module.params["crl_mode"] == "update"
self.ignore_timestamps = module.params["ignore_timestamps"]
self.return_content = module.params["return_content"]
self.name_encoding = module.params["name_encoding"]
self.serial_numbers_format = module.params["serial_numbers"]
self.crl_content = None
self.privatekey_path = module.params['privatekey_path']
self.privatekey_content = module.params['privatekey_content']
self.privatekey_path = module.params["privatekey_path"]
self.privatekey_content = module.params["privatekey_content"]
if self.privatekey_content is not None:
self.privatekey_content = self.privatekey_content.encode('utf-8')
self.privatekey_passphrase = module.params['privatekey_passphrase']
self.privatekey_content = self.privatekey_content.encode("utf-8")
self.privatekey_passphrase = module.params["privatekey_passphrase"]
try:
if module.params['issuer_ordered']:
if module.params["issuer_ordered"]:
self.issuer_ordered = True
self.issuer = parse_ordered_name_field(module.params['issuer_ordered'], 'issuer_ordered')
self.issuer = parse_ordered_name_field(
module.params["issuer_ordered"], "issuer_ordered"
)
else:
self.issuer_ordered = False
self.issuer = parse_name_field(module.params['issuer'], 'issuer')
self.issuer = parse_name_field(module.params["issuer"], "issuer")
except (TypeError, ValueError) as exc:
module.fail_json(msg=to_native(exc))
self.last_update = get_relative_time_option(module.params['last_update'], 'last_update', with_timezone=CRYPTOGRAPHY_TIMEZONE)
self.next_update = get_relative_time_option(module.params['next_update'], 'next_update', with_timezone=CRYPTOGRAPHY_TIMEZONE)
self.last_update = get_relative_time_option(
module.params["last_update"],
"last_update",
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
self.next_update = get_relative_time_option(
module.params["next_update"],
"next_update",
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
self.digest = select_message_digest(module.params['digest'])
self.digest = select_message_digest(module.params["digest"])
if self.digest is None:
raise CRLError('The digest "{0}" is not supported'.format(module.params['digest']))
raise CRLError(
'The digest "{0}" is not supported'.format(module.params["digest"])
)
self.module = module
self.revoked_certificates = []
for i, rc in enumerate(module.params['revoked_certificates']):
for i, rc in enumerate(module.params["revoked_certificates"]):
result = {
'serial_number': None,
'revocation_date': None,
'issuer': None,
'issuer_critical': False,
'reason': None,
'reason_critical': False,
'invalidity_date': None,
'invalidity_date_critical': False,
"serial_number": None,
"revocation_date": None,
"issuer": None,
"issuer_critical": False,
"reason": None,
"reason_critical": False,
"invalidity_date": None,
"invalidity_date_critical": False,
}
path_prefix = 'revoked_certificates[{0}].'.format(i)
if rc['path'] is not None or rc['content'] is not None:
path_prefix = "revoked_certificates[{0}].".format(i)
if rc["path"] is not None or rc["content"] is not None:
# Load certificate from file or content
try:
if rc['content'] is not None:
rc['content'] = rc['content'].encode('utf-8')
cert = load_certificate(rc['path'], content=rc['content'], backend='cryptography')
result['serial_number'] = cryptography_serial_number_of_cert(cert)
if rc["content"] is not None:
rc["content"] = rc["content"].encode("utf-8")
cert = load_certificate(
rc["path"], content=rc["content"], backend="cryptography"
)
result["serial_number"] = cryptography_serial_number_of_cert(cert)
except OpenSSLObjectError as e:
if rc['content'] is not None:
if rc["content"] is not None:
module.fail_json(
msg='Cannot parse certificate from {0}content: {1}'.format(path_prefix, to_native(e))
msg="Cannot parse certificate from {0}content: {1}".format(
path_prefix, to_native(e)
)
)
else:
module.fail_json(
msg='Cannot read certificate "{1}" from {0}path: {2}'.format(path_prefix, rc['path'], to_native(e))
msg='Cannot read certificate "{1}" from {0}path: {2}'.format(
path_prefix, rc["path"], to_native(e)
)
)
else:
# Specify serial_number (and potentially issuer) directly
result['serial_number'] = self._parse_serial_number(rc['serial_number'], i)
result["serial_number"] = self._parse_serial_number(
rc["serial_number"], i
)
# All other options
if rc['issuer']:
result['issuer'] = [cryptography_get_name(issuer, 'issuer') for issuer in rc['issuer']]
result['issuer_critical'] = rc['issuer_critical']
result['revocation_date'] = get_relative_time_option(
rc['revocation_date'],
path_prefix + 'revocation_date',
if rc["issuer"]:
result["issuer"] = [
cryptography_get_name(issuer, "issuer") for issuer in rc["issuer"]
]
result["issuer_critical"] = rc["issuer_critical"]
result["revocation_date"] = get_relative_time_option(
rc["revocation_date"],
path_prefix + "revocation_date",
with_timezone=CRYPTOGRAPHY_TIMEZONE,
)
if rc['reason']:
result['reason'] = REVOCATION_REASON_MAP[rc['reason']]
result['reason_critical'] = rc['reason_critical']
if rc['invalidity_date']:
result['invalidity_date'] = get_relative_time_option(
rc['invalidity_date'],
path_prefix + 'invalidity_date',
if rc["reason"]:
result["reason"] = REVOCATION_REASON_MAP[rc["reason"]]
result["reason_critical"] = rc["reason_critical"]
if rc["invalidity_date"]:
result["invalidity_date"] = get_relative_time_option(
rc["invalidity_date"],
path_prefix + "invalidity_date",
with_timezone=CRYPTOGRAPHY_TIMEZONE_INVALIDITY_DATE,
)
result['invalidity_date_critical'] = rc['invalidity_date_critical']
result["invalidity_date_critical"] = rc["invalidity_date_critical"]
self.revoked_certificates.append(result)
self.backup = module.params['backup']
self.backup = module.params["backup"]
self.backup_file = None
try:
@@ -627,17 +650,17 @@ class CRL(OpenSSLObject):
path=self.privatekey_path,
content=self.privatekey_content,
passphrase=self.privatekey_passphrase,
backend='cryptography'
backend="cryptography",
)
except OpenSSLBadPassphraseError as exc:
raise CRLError(exc)
self.crl = None
try:
with open(self.path, 'rb') as f:
with open(self.path, "rb") as f:
data = f.read()
self.actual_format = 'pem' if identify_pem_format(data) else 'der'
if self.actual_format == 'pem':
self.actual_format = "pem" if identify_pem_format(data) else "der"
if self.actual_format == "pem":
self.crl = x509.load_pem_x509_crl(data, default_backend())
if self.return_content:
self.crl_content = data
@@ -653,30 +676,36 @@ class CRL(OpenSSLObject):
self.diff_after = self.diff_before = self._get_info(data)
def _parse_serial_number(self, value, index):
if self.serial_numbers_format == 'integer':
if self.serial_numbers_format == "integer":
try:
return check_type_int(value)
except TypeError as exc:
self.module.fail_json(msg='Error while parsing revoked_certificates[{idx}].serial_number as an integer: {exc}'.format(
idx=index + 1,
exc=to_native(exc),
))
if self.serial_numbers_format == 'hex-octets':
self.module.fail_json(
msg="Error while parsing revoked_certificates[{idx}].serial_number as an integer: {exc}".format(
idx=index + 1,
exc=to_native(exc),
)
)
if self.serial_numbers_format == "hex-octets":
try:
return parse_serial(check_type_str(value))
except (TypeError, ValueError) as exc:
self.module.fail_json(msg='Error while parsing revoked_certificates[{idx}].serial_number as an colon-separated hex octet string: {exc}'.format(
idx=index + 1,
exc=to_native(exc),
))
raise RuntimeError('Unexpected value %s of serial_numbers' % (self.serial_numbers_format, ))
self.module.fail_json(
msg="Error while parsing revoked_certificates[{idx}].serial_number as an colon-separated hex octet string: {exc}".format(
idx=index + 1,
exc=to_native(exc),
)
)
raise RuntimeError(
"Unexpected value %s of serial_numbers" % (self.serial_numbers_format,)
)
def _get_info(self, data):
if data is None:
return dict()
try:
result = get_crl_info(self.module, data)
result['can_parse_crl'] = True
result["can_parse_crl"] = True
return result
except Exception:
return dict(can_parse_crl=False)
@@ -688,35 +717,38 @@ class CRL(OpenSSLObject):
def _compress_entry(self, entry):
issuer = None
if entry['issuer'] is not None:
if entry["issuer"] is not None:
# Normalize to IDNA. If this is used-provided, it was already converted to
# IDNA (by cryptography_get_name) and thus the `idna` library is present.
# If this is coming from cryptography and is not already in IDNA (i.e. ascii),
# cryptography < 2.1 must be in use, which depends on `idna`. So this should
# not require `idna` except if it was already used by code earlier during
# this invocation.
issuer = tuple(cryptography_decode_name(issuer, idn_rewrite='idna') for issuer in entry['issuer'])
issuer = tuple(
cryptography_decode_name(issuer, idn_rewrite="idna")
for issuer in entry["issuer"]
)
if self.ignore_timestamps:
# Throw out revocation_date
return (
entry['serial_number'],
entry["serial_number"],
issuer,
entry['issuer_critical'],
entry['reason'],
entry['reason_critical'],
entry['invalidity_date'],
entry['invalidity_date_critical'],
entry["issuer_critical"],
entry["reason"],
entry["reason_critical"],
entry["invalidity_date"],
entry["invalidity_date_critical"],
)
else:
return (
entry['serial_number'],
entry['revocation_date'],
entry["serial_number"],
entry["revocation_date"],
issuer,
entry['issuer_critical'],
entry['reason'],
entry['reason_critical'],
entry['invalidity_date'],
entry['invalidity_date_critical'],
entry["issuer_critical"],
entry["reason"],
entry["reason_critical"],
entry["invalidity_date"],
entry["invalidity_date_critical"],
)
def check(self, module, perms_required=True, ignore_conversion=True):
@@ -735,13 +767,18 @@ class CRL(OpenSSLObject):
if self.next_update != get_next_update(self.crl) and not self.ignore_timestamps:
return False
if cryptography_key_needs_digest_for_signing(self.privatekey):
if self.crl.signature_hash_algorithm is None or self.digest.name != self.crl.signature_hash_algorithm.name:
if (
self.crl.signature_hash_algorithm is None
or self.digest.name != self.crl.signature_hash_algorithm.name
):
return False
else:
if self.crl.signature_hash_algorithm is not None:
return False
want_issuer = [(cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer]
want_issuer = [
(cryptography_name_to_oid(entry[0]), entry[1]) for entry in self.issuer
]
is_issuer = [(sub.oid, sub.value) for sub in self.crl.issuer]
if not self.issuer_ordered:
want_issuer = set(want_issuer)
@@ -749,7 +786,10 @@ class CRL(OpenSSLObject):
if want_issuer != is_issuer:
return False
old_entries = [self._compress_entry(cryptography_decode_revoked_certificate(cert)) for cert in self.crl]
old_entries = [
self._compress_entry(cryptography_decode_revoked_certificate(cert))
for cert in self.crl
]
new_entries = [self._compress_entry(cert) for cert in self.revoked_certificates]
if self.update:
# We do not simply use a set so that duplicate entries are treated correctly
@@ -772,10 +812,16 @@ class CRL(OpenSSLObject):
crl = CertificateRevocationListBuilder()
try:
crl = crl.issuer_name(Name([
NameAttribute(cryptography_name_to_oid(entry[0]), to_text(entry[1]))
for entry in self.issuer
]))
crl = crl.issuer_name(
Name(
[
NameAttribute(
cryptography_name_to_oid(entry[0]), to_text(entry[1])
)
for entry in self.issuer
]
)
)
except ValueError as e:
raise CRLError(e)
@@ -783,29 +829,31 @@ class CRL(OpenSSLObject):
crl = set_next_update(crl, self.next_update)
if self.update and self.crl:
new_entries = set([self._compress_entry(entry) for entry in self.revoked_certificates])
new_entries = set(
[self._compress_entry(entry) for entry in self.revoked_certificates]
)
for entry in self.crl:
decoded_entry = self._compress_entry(cryptography_decode_revoked_certificate(entry))
decoded_entry = self._compress_entry(
cryptography_decode_revoked_certificate(entry)
)
if decoded_entry not in new_entries:
crl = crl.add_revoked_certificate(entry)
for entry in self.revoked_certificates:
revoked_cert = RevokedCertificateBuilder()
revoked_cert = revoked_cert.serial_number(entry['serial_number'])
revoked_cert = set_revocation_date(revoked_cert, entry['revocation_date'])
if entry['issuer'] is not None:
revoked_cert = revoked_cert.serial_number(entry["serial_number"])
revoked_cert = set_revocation_date(revoked_cert, entry["revocation_date"])
if entry["issuer"] is not None:
revoked_cert = revoked_cert.add_extension(
x509.CertificateIssuer(entry['issuer']),
entry['issuer_critical']
x509.CertificateIssuer(entry["issuer"]), entry["issuer_critical"]
)
if entry['reason'] is not None:
if entry["reason"] is not None:
revoked_cert = revoked_cert.add_extension(
x509.CRLReason(entry['reason']),
entry['reason_critical']
x509.CRLReason(entry["reason"]), entry["reason_critical"]
)
if entry['invalidity_date'] is not None:
if entry["invalidity_date"] is not None:
revoked_cert = revoked_cert.add_extension(
x509.InvalidityDate(entry['invalidity_date']),
entry['invalidity_date_critical']
x509.InvalidityDate(entry["invalidity_date"]),
entry["invalidity_date_critical"],
)
crl = crl.add_revoked_certificate(revoked_cert.build(backend))
@@ -813,17 +861,23 @@ class CRL(OpenSSLObject):
if cryptography_key_needs_digest_for_signing(self.privatekey):
digest = self.digest
self.crl = crl.sign(self.privatekey, digest, backend=backend)
if self.format == 'pem':
if self.format == "pem":
return self.crl.public_bytes(Encoding.PEM)
else:
return self.crl.public_bytes(Encoding.DER)
def generate(self):
result = None
if not self.check(self.module, perms_required=False, ignore_conversion=True) or self.force:
if (
not self.check(self.module, perms_required=False, ignore_conversion=True)
or self.force
):
result = self._generate_crl()
elif not self.check(self.module, perms_required=False, ignore_conversion=False) and self.crl:
if self.format == 'pem':
elif (
not self.check(self.module, perms_required=False, ignore_conversion=False)
and self.crl
):
if self.format == "pem":
result = self.crl.public_bytes(Encoding.PEM)
else:
result = self.crl.public_bytes(Encoding.DER)
@@ -831,7 +885,7 @@ class CRL(OpenSSLObject):
if result is not None:
self.diff_after = self._get_info(result)
if self.return_content:
if self.format == 'pem':
if self.format == "pem":
self.crl_content = result
else:
self.crl_content = base64.b64encode(result)
@@ -841,59 +895,67 @@ class CRL(OpenSSLObject):
self.changed = True
file_args = self.module.load_file_common_arguments(self.module.params)
if self.module.check_file_absent_if_check_mode(file_args['path']):
if self.module.check_file_absent_if_check_mode(file_args["path"]):
self.changed = True
elif self.module.set_fs_attributes_if_different(file_args, False):
self.changed = True
def dump(self, check_mode=False):
result = {
'changed': self.changed,
'filename': self.path,
'privatekey': self.privatekey_path,
'format': self.format,
'last_update': None,
'next_update': None,
'digest': None,
'issuer_ordered': None,
'issuer': None,
'revoked_certificates': [],
"changed": self.changed,
"filename": self.path,
"privatekey": self.privatekey_path,
"format": self.format,
"last_update": None,
"next_update": None,
"digest": None,
"issuer_ordered": None,
"issuer": None,
"revoked_certificates": [],
}
if self.backup_file:
result['backup_file'] = self.backup_file
result["backup_file"] = self.backup_file
if check_mode:
result['last_update'] = self.last_update.strftime(TIMESTAMP_FORMAT)
result['next_update'] = self.next_update.strftime(TIMESTAMP_FORMAT)
result["last_update"] = self.last_update.strftime(TIMESTAMP_FORMAT)
result["next_update"] = self.next_update.strftime(TIMESTAMP_FORMAT)
# result['digest'] = cryptography_oid_to_name(self.crl.signature_algorithm_oid)
result['digest'] = self.module.params['digest']
result['issuer_ordered'] = self.issuer
result['issuer'] = {}
result["digest"] = self.module.params["digest"]
result["issuer_ordered"] = self.issuer
result["issuer"] = {}
for k, v in self.issuer:
result['issuer'][k] = v
result['revoked_certificates'] = []
result["issuer"][k] = v
result["revoked_certificates"] = []
for entry in self.revoked_certificates:
result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding))
result["revoked_certificates"].append(
cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding)
)
elif self.crl:
result['last_update'] = get_last_update(self.crl).strftime(TIMESTAMP_FORMAT)
result['next_update'] = get_next_update(self.crl).strftime(TIMESTAMP_FORMAT)
result['digest'] = cryptography_oid_to_name(cryptography_get_signature_algorithm_oid_from_crl(self.crl))
result["last_update"] = get_last_update(self.crl).strftime(TIMESTAMP_FORMAT)
result["next_update"] = get_next_update(self.crl).strftime(TIMESTAMP_FORMAT)
result["digest"] = cryptography_oid_to_name(
cryptography_get_signature_algorithm_oid_from_crl(self.crl)
)
issuer = []
for attribute in self.crl.issuer:
issuer.append([cryptography_oid_to_name(attribute.oid), attribute.value])
result['issuer_ordered'] = issuer
result['issuer'] = {}
issuer.append(
[cryptography_oid_to_name(attribute.oid), attribute.value]
)
result["issuer_ordered"] = issuer
result["issuer"] = {}
for k, v in issuer:
result['issuer'][k] = v
result['revoked_certificates'] = []
result["issuer"][k] = v
result["revoked_certificates"] = []
for cert in self.crl:
entry = cryptography_decode_revoked_certificate(cert)
result['revoked_certificates'].append(cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding))
result["revoked_certificates"].append(
cryptography_dump_revoked(entry, idn_rewrite=self.name_encoding)
)
if self.return_content:
result['crl'] = self.crl_content
result["crl"] = self.crl_content
result['diff'] = dict(
result["diff"] = dict(
before=self.diff_before,
after=self.diff_after,
)
@@ -903,100 +965,121 @@ class CRL(OpenSSLObject):
def main():
module = AnsibleModule(
argument_spec=dict(
state=dict(type='str', default='present', choices=['present', 'absent']),
state=dict(type="str", default="present", choices=["present", "absent"]),
crl_mode=dict(
type='str',
type="str",
# default='generate',
choices=['generate', 'update'],
choices=["generate", "update"],
),
mode=dict(
type='str',
type="str",
# default='generate',
choices=['generate', 'update'],
removed_in_version='3.0.0',
removed_from_collection='community.crypto',
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),
format=dict(type='str', default='pem', choices=['pem', 'der']),
privatekey_path=dict(type='path'),
privatekey_content=dict(type='str', no_log=True),
privatekey_passphrase=dict(type='str', no_log=True),
issuer=dict(type='dict'),
issuer_ordered=dict(type='list', elements='dict'),
last_update=dict(type='str', default='+0s'),
next_update=dict(type='str'),
digest=dict(type='str', default='sha256'),
ignore_timestamps=dict(type='bool', default=False),
return_content=dict(type='bool', default=False),
force=dict(type="bool", default=False),
backup=dict(type="bool", default=False),
path=dict(type="path", required=True),
format=dict(type="str", default="pem", choices=["pem", "der"]),
privatekey_path=dict(type="path"),
privatekey_content=dict(type="str", no_log=True),
privatekey_passphrase=dict(type="str", no_log=True),
issuer=dict(type="dict"),
issuer_ordered=dict(type="list", elements="dict"),
last_update=dict(type="str", default="+0s"),
next_update=dict(type="str"),
digest=dict(type="str", default="sha256"),
ignore_timestamps=dict(type="bool", default=False),
return_content=dict(type="bool", default=False),
revoked_certificates=dict(
type='list',
elements='dict',
type="list",
elements="dict",
options=dict(
path=dict(type='path'),
content=dict(type='str'),
serial_number=dict(type='raw'),
revocation_date=dict(type='str', default='+0s'),
issuer=dict(type='list', elements='str'),
issuer_critical=dict(type='bool', default=False),
path=dict(type="path"),
content=dict(type="str"),
serial_number=dict(type="raw"),
revocation_date=dict(type="str", default="+0s"),
issuer=dict(type="list", elements="str"),
issuer_critical=dict(type="bool", default=False),
reason=dict(
type='str',
type="str",
choices=[
'unspecified', 'key_compromise', 'ca_compromise', 'affiliation_changed',
'superseded', 'cessation_of_operation', 'certificate_hold',
'privilege_withdrawn', 'aa_compromise', 'remove_from_crl'
]
"unspecified",
"key_compromise",
"ca_compromise",
"affiliation_changed",
"superseded",
"cessation_of_operation",
"certificate_hold",
"privilege_withdrawn",
"aa_compromise",
"remove_from_crl",
],
),
reason_critical=dict(type='bool', default=False),
invalidity_date=dict(type='str'),
invalidity_date_critical=dict(type='bool', default=False),
reason_critical=dict(type="bool", default=False),
invalidity_date=dict(type="str"),
invalidity_date_critical=dict(type="bool", default=False),
),
required_one_of=[['path', 'content', 'serial_number']],
mutually_exclusive=[['path', 'content', 'serial_number']],
required_one_of=[["path", "content", "serial_number"]],
mutually_exclusive=[["path", "content", "serial_number"]],
),
name_encoding=dict(
type="str", default="ignore", choices=["ignore", "idna", "unicode"]
),
serial_numbers=dict(
type="str", default="integer", choices=["integer", "hex-octets"]
),
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
serial_numbers=dict(type='str', default='integer', choices=['integer', 'hex-octets']),
),
required_if=[
('state', 'present', ['privatekey_path', 'privatekey_content'], True),
('state', 'present', ['issuer', 'issuer_ordered'], True),
('state', 'present', ['next_update', 'revoked_certificates'], False),
("state", "present", ["privatekey_path", "privatekey_content"], True),
("state", "present", ["issuer", "issuer_ordered"], True),
("state", "present", ["next_update", "revoked_certificates"], False),
],
mutually_exclusive=(
['privatekey_path', 'privatekey_content'],
['issuer', 'issuer_ordered'],
["privatekey_path", "privatekey_content"],
["issuer", "issuer_ordered"],
),
supports_check_mode=True,
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']
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)
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('cryptography >= {0}'.format(MINIMAL_CRYPTOGRAPHY_VERSION)),
exception=CRYPTOGRAPHY_IMP_ERR)
module.fail_json(
msg=missing_required_lib(
"cryptography >= {0}".format(MINIMAL_CRYPTOGRAPHY_VERSION)
),
exception=CRYPTOGRAPHY_IMP_ERR,
)
try:
crl = CRL(module)
if module.params['state'] == 'present':
if module.params["state"] == "present":
if module.check_mode:
result = crl.dump(check_mode=True)
result['changed'] = module.params['force'] or not crl.check(module) or not crl.check(module, ignore_conversion=False)
result["changed"] = (
module.params["force"]
or not crl.check(module)
or not crl.check(module, ignore_conversion=False)
)
module.exit_json(**result)
crl.generate()
else:
if module.check_mode:
result = crl.dump(check_mode=True)
result['changed'] = os.path.exists(module.params['path'])
result["changed"] = os.path.exists(module.params["path"])
module.exit_json(**result)
crl.remove()

View File

@@ -195,36 +195,42 @@ from ansible_collections.community.crypto.plugins.module_utils.crypto.pem import
def main():
module = AnsibleModule(
argument_spec=dict(
path=dict(type='path'),
content=dict(type='str'),
list_revoked_certificates=dict(type='bool', default=True),
name_encoding=dict(type='str', default='ignore', choices=['ignore', 'idna', 'unicode']),
),
required_one_of=(
['path', 'content'],
),
mutually_exclusive=(
['path', 'content'],
path=dict(type="path"),
content=dict(type="str"),
list_revoked_certificates=dict(type="bool", default=True),
name_encoding=dict(
type="str", default="ignore", choices=["ignore", "idna", "unicode"]
),
),
required_one_of=(["path", "content"],),
mutually_exclusive=(["path", "content"],),
supports_check_mode=True,
)
if module.params['content'] is None:
if module.params["content"] is None:
try:
with open(module.params['path'], 'rb') as f:
with open(module.params["path"], "rb") as f:
data = f.read()
except (IOError, OSError) as e:
module.fail_json(msg='Error while reading CRL file from disk: {0}'.format(e))
module.fail_json(
msg="Error while reading CRL file from disk: {0}".format(e)
)
else:
data = module.params['content'].encode('utf-8')
data = module.params["content"].encode("utf-8")
if not identify_pem_format(data):
try:
data = base64.b64decode(module.params['content'])
data = base64.b64decode(module.params["content"])
except (binascii.Error, TypeError) as e:
module.fail_json(msg='Error while Base64 decoding content: {0}'.format(e))
module.fail_json(
msg="Error while Base64 decoding content: {0}".format(e)
)
try:
result = get_crl_info(module, data, list_revoked_certificates=module.params['list_revoked_certificates'])
result = get_crl_info(
module,
data,
list_revoked_certificates=module.params["list_revoked_certificates"],
)
module.exit_json(**result)
except OpenSSLObjectError as e:
module.fail_json(msg=to_native(e))