mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-07-23 13:20:23 -07:00
ACME: use Cryptography (if a new enough version is available) instead of OpenSSL (#42170)
* Collecting PEM -> DER conversions. * Using cryptography instead of OpenSSL binary in some situations. * Moving key-to-disk writing for key content to parse_account_key. * Rename parse_account_key -> parse_key. * Move OpenSSL specific code for key parsing and request signing into global functions. * Also using cryptography for key parsing and request signing. * Remove assert statements. * Fixing handling of key contents for cryptography code path. * Allow to disable the use of cryptography. * Updating documentation. * 1.5 seems to work as well (earlier versions don't have EC sign function). Making Python 2.x adjustments. * Changing option to select_crypto_backend. * Python 2.6 compatibility. * Trying to test both backends separately for acme_account. * Also testing both backends separately for acme_certificate and acme_certificate_revoke. * Adding changelog entry which informs about select_crypto_backend option in case autodetect fails. * Fixing YAML.
This commit is contained in:
parent
7f41f0168a
commit
aef16ee195
13 changed files with 1031 additions and 671 deletions
|
@ -116,15 +116,10 @@ account_uri:
|
|||
'''
|
||||
|
||||
from ansible.module_utils.acme import (
|
||||
ModuleFailException, ACMEAccount
|
||||
ModuleFailException, ACMEAccount, set_crypto_backend,
|
||||
)
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -141,6 +136,7 @@ def main():
|
|||
contact=dict(required=False, type='list', default=[]),
|
||||
new_account_key_src=dict(type='path'),
|
||||
new_account_key_content=dict(type='str', no_log=True),
|
||||
select_crypto_backend=dict(required=False, choices=['auto', 'openssl', 'cryptography'], default='auto', type='str'),
|
||||
),
|
||||
required_one_of=(
|
||||
['account_key_src', 'account_key_content'],
|
||||
|
@ -156,6 +152,7 @@ def main():
|
|||
),
|
||||
supports_check_mode=True,
|
||||
)
|
||||
set_crypto_backend(module)
|
||||
|
||||
if not module.params.get('validate_certs'):
|
||||
module.warn(warning='Disabling certificate validation for communications with ACME endpoint. ' +
|
||||
|
@ -203,24 +200,11 @@ def main():
|
|||
raise ModuleFailException(msg='Account does not exist or is deactivated.')
|
||||
module.exit_json(changed=changed, account_uri=account.uri)
|
||||
elif state == 'changed_key':
|
||||
# Get hold of new account key
|
||||
new_key = module.params.get('new_account_key_src')
|
||||
if new_key is None:
|
||||
fd, tmpsrc = tempfile.mkstemp()
|
||||
module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
|
||||
f = os.fdopen(fd, 'wb')
|
||||
try:
|
||||
f.write(module.params.get('new_account_key_content').encode('utf-8'))
|
||||
new_key = tmpsrc
|
||||
except Exception as err:
|
||||
try:
|
||||
f.close()
|
||||
except Exception as e:
|
||||
pass
|
||||
raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
||||
f.close()
|
||||
# Parse new account key
|
||||
error, new_key_data = account.parse_account_key(new_key)
|
||||
error, new_key_data = account.parse_key(
|
||||
module.params.get('new_account_key_src'),
|
||||
module.params.get('new_account_key_content')
|
||||
)
|
||||
if error:
|
||||
raise ModuleFailException("error while parsing account key: %s" % error)
|
||||
# Verify that the account exists and has not been deactivated
|
||||
|
@ -249,7 +233,7 @@ def main():
|
|||
"oldKey": account.jwk, # discussed in https://github.com/ietf-wg-acme/acme/pull/425,
|
||||
# might be required in draft 13
|
||||
}
|
||||
data = account.sign_request(protected, payload, new_key_data, new_key)
|
||||
data = account.sign_request(protected, payload, new_key_data)
|
||||
# Send request and verify result
|
||||
result, info = account.send_signed_request(url, data)
|
||||
if info['status'] != 200:
|
||||
|
|
|
@ -328,7 +328,9 @@ account_uri:
|
|||
'''
|
||||
|
||||
from ansible.module_utils.acme import (
|
||||
ModuleFailException, fetch_url, write_file, nopad_b64, simple_get, ACMEAccount
|
||||
ModuleFailException, fetch_url, write_file, nopad_b64, simple_get, pem_to_der, ACMEAccount,
|
||||
HAS_CURRENT_CRYPTOGRAPHY, cryptography_get_csr_domains, cryptography_get_cert_days,
|
||||
set_crypto_backend,
|
||||
)
|
||||
|
||||
import base64
|
||||
|
@ -350,6 +352,8 @@ def get_cert_days(module, cert_file):
|
|||
if the file was not found. If cert_file contains more than one
|
||||
certificate, only the first one will be considered.
|
||||
'''
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
return cryptography_get_cert_days(module, cert_file)
|
||||
if not os.path.exists(cert_file):
|
||||
return -1
|
||||
|
||||
|
@ -422,6 +426,8 @@ class ACMEClient(object):
|
|||
'''
|
||||
Parse the CSR and return the list of requested domains
|
||||
'''
|
||||
if HAS_CURRENT_CRYPTOGRAPHY:
|
||||
return cryptography_get_csr_domains(self.module, self.csr)
|
||||
openssl_csr_cmd = [self._openssl_bin, "req", "-in", self.csr, "-noout", "-text"]
|
||||
dummy, out, dummy = self.module.run_command(openssl_csr_cmd, check_rc=True)
|
||||
|
||||
|
@ -569,11 +575,9 @@ class ACMEClient(object):
|
|||
Return the certificate object as dict
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme-12#section-7.4
|
||||
'''
|
||||
openssl_csr_cmd = [self._openssl_bin, "req", "-in", self.csr, "-outform", "DER"]
|
||||
dummy, out, dummy = self.module.run_command(openssl_csr_cmd, check_rc=True)
|
||||
|
||||
csr = pem_to_der(self.csr)
|
||||
new_cert = {
|
||||
"csr": nopad_b64(to_bytes(out)),
|
||||
"csr": nopad_b64(csr),
|
||||
}
|
||||
result, info = self.account.send_signed_request(self.finalize_uri, new_cert)
|
||||
if info['status'] not in [200]:
|
||||
|
@ -650,12 +654,10 @@ class ACMEClient(object):
|
|||
Return the certificate object as dict
|
||||
https://tools.ietf.org/html/draft-ietf-acme-acme-02#section-6.5
|
||||
'''
|
||||
openssl_csr_cmd = [self._openssl_bin, "req", "-in", self.csr, "-outform", "DER"]
|
||||
dummy, out, dummy = self.module.run_command(openssl_csr_cmd, check_rc=True)
|
||||
|
||||
csr = pem_to_der(self.csr)
|
||||
new_cert = {
|
||||
"resource": "new-cert",
|
||||
"csr": nopad_b64(to_bytes(out)),
|
||||
"csr": nopad_b64(csr),
|
||||
}
|
||||
result, info = self.account.send_signed_request(self.directory['new-cert'], new_cert)
|
||||
|
||||
|
@ -883,6 +885,7 @@ def main():
|
|||
remaining_days=dict(required=False, default=10, type='int'),
|
||||
deactivate_authzs=dict(required=False, default=False, type='bool'),
|
||||
force=dict(required=False, default=False, type='bool'),
|
||||
select_crypto_backend=dict(required=False, choices=['auto', 'openssl', 'cryptography'], default='auto', type='str'),
|
||||
),
|
||||
required_one_of=(
|
||||
['account_key_src', 'account_key_content'],
|
||||
|
@ -895,6 +898,7 @@ def main():
|
|||
)
|
||||
if module._name == 'letsencrypt':
|
||||
module.deprecate("The 'letsencrypt' module is being renamed 'acme_certificate'", version='2.10')
|
||||
set_crypto_backend(module)
|
||||
|
||||
# AnsibleModule() changes the locale, so change it back to C because we rely on time.strptime() when parsing certificate dates.
|
||||
module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C')
|
||||
|
|
|
@ -53,6 +53,10 @@ options:
|
|||
important private key — it can be used to change the account key,
|
||||
or to revoke your certificates without knowing their private keys
|
||||
—, this might not be acceptable."
|
||||
- "In case C(cryptography) is used, the content is not written into a
|
||||
temporary file. It can still happen that it is written to disk by
|
||||
Ansible in the process of moving the module with its argument to
|
||||
the node where it is executed."
|
||||
revoke_reason:
|
||||
description:
|
||||
- "One of the revocation reasonCodes defined in
|
||||
|
@ -80,16 +84,10 @@ RETURN = '''
|
|||
'''
|
||||
|
||||
from ansible.module_utils.acme import (
|
||||
ModuleFailException, ACMEAccount, nopad_b64
|
||||
ModuleFailException, ACMEAccount, nopad_b64, pem_to_der, set_crypto_backend,
|
||||
)
|
||||
|
||||
import base64
|
||||
import os
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
|
||||
def main():
|
||||
|
@ -104,6 +102,7 @@ def main():
|
|||
private_key_content=dict(type='str', no_log=True),
|
||||
certificate=dict(required=True, type='path'),
|
||||
revoke_reason=dict(required=False, type='int'),
|
||||
select_crypto_backend=dict(required=False, choices=['auto', 'openssl', 'cryptography'], default='auto', type='str'),
|
||||
),
|
||||
required_one_of=(
|
||||
['account_key_src', 'account_key_content', 'private_key_src', 'private_key_content'],
|
||||
|
@ -113,6 +112,7 @@ def main():
|
|||
),
|
||||
supports_check_mode=False,
|
||||
)
|
||||
set_crypto_backend(module)
|
||||
|
||||
if not module.params.get('validate_certs'):
|
||||
module.warn(warning='Disabling certificate validation for communications with ACME endpoint. ' +
|
||||
|
@ -122,22 +122,8 @@ def main():
|
|||
try:
|
||||
account = ACMEAccount(module)
|
||||
# Load certificate
|
||||
certificate_lines = []
|
||||
try:
|
||||
with open(module.params.get('certificate'), "rt") as f:
|
||||
header_line_count = 0
|
||||
for line in f:
|
||||
if line.startswith('-----'):
|
||||
header_line_count += 1
|
||||
if header_line_count == 2:
|
||||
# If certificate file contains other certs appended
|
||||
# (like intermediate certificates), ignore these.
|
||||
break
|
||||
continue
|
||||
certificate_lines.append(line.strip())
|
||||
except Exception as err:
|
||||
raise ModuleFailException("cannot load certificate file: %s" % to_native(err), exception=traceback.format_exc())
|
||||
certificate = nopad_b64(base64.b64decode(''.join(certificate_lines)))
|
||||
certificate = pem_to_der(module.params.get('certificate'))
|
||||
certificate = nopad_b64(certificate)
|
||||
# Construct payload
|
||||
payload = {
|
||||
'certificate': certificate
|
||||
|
@ -152,24 +138,11 @@ def main():
|
|||
endpoint = account.directory['revokeCert']
|
||||
# Get hold of private key (if available) and make sure it comes from disk
|
||||
private_key = module.params.get('private_key_src')
|
||||
if module.params.get('private_key_content') is not None:
|
||||
fd, tmpsrc = tempfile.mkstemp()
|
||||
module.add_cleanup_file(tmpsrc) # Ansible will delete the file on exit
|
||||
f = os.fdopen(fd, 'wb')
|
||||
try:
|
||||
f.write(module.params.get('private_key_content').encode('utf-8'))
|
||||
private_key = tmpsrc
|
||||
except Exception as err:
|
||||
try:
|
||||
f.close()
|
||||
except Exception as e:
|
||||
pass
|
||||
raise ModuleFailException("failed to create temporary content file: %s" % to_native(err), exception=traceback.format_exc())
|
||||
f.close()
|
||||
private_key_content = module.params.get('private_key_content')
|
||||
# Revoke certificate
|
||||
if private_key:
|
||||
if private_key or private_key_content:
|
||||
# Step 1: load and parse private key
|
||||
error, private_key_data = account.parse_account_key(private_key)
|
||||
error, private_key_data = account.parse_key(private_key, private_key_content)
|
||||
if error:
|
||||
raise ModuleFailException("error while parsing private key: %s" % error)
|
||||
# Step 2: sign revokation request with private key
|
||||
|
@ -177,8 +150,7 @@ def main():
|
|||
"alg": private_key_data['alg'],
|
||||
"jwk": private_key_data['jwk'],
|
||||
}
|
||||
result, info = account.send_signed_request(endpoint, payload, key=private_key,
|
||||
key_data=private_key_data, jws_header=jws_header)
|
||||
result, info = account.send_signed_request(endpoint, payload, key_data=private_key_data, jws_header=jws_header)
|
||||
else:
|
||||
# Step 1: get hold of account URI
|
||||
changed = account.init_account(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue