Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable TLS/SSL CTX Options for the get_certificate Module #779

Merged
merged 11 commits into from
Jul 7, 2024
4 changes: 4 additions & 0 deletions changelogs/fragments/779-add-tls_ctx_options-option.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
minor_changes:
- get_certificate - adds ``tls_ctx_options`` option for specifying SSL CTX options (https:/ansible-collections/community.crypto/pull/779).
...
67 changes: 66 additions & 1 deletion plugins/modules/get_certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@
- The default value V(false) is B(deprecated) and will change to V(true) in community.crypto 3.0.0.
type: bool
version_added: 2.12.0
tls_ctx_options:
description:
- TLS CTX options (TLS/SSL OP flags) to use for the request.
felixfontein marked this conversation as resolved.
Show resolved Hide resolved
- See the L(List of SSL OP Flags,https://wiki.openssl.org/index.php/List_of_SSL_OP_Flags) for more details.
- The available TLS CTX options is dependent on the Python and OpenSSL/LibreSSL versions.
type: list
elements: raw
version_added: 2.21.0

notes:
- When using ca_cert on OS X it has been reported that in some conditions the validate will always succeed.
Expand Down Expand Up @@ -205,18 +213,37 @@
msg: "cert expires in: {{ expire_days }} days."
vars:
expire_days: "{{ (( cert.not_after | to_datetime('%Y%m%d%H%M%SZ')) - (ansible_date_time.iso8601 | to_datetime('%Y-%m-%dT%H:%M:%SZ')) ).days }}"

- name: Allow legacy insecure renegotiation to get a cert from a legacy device
community.crypto.get_certificate:
host: "legacy-device.domain.com"
port: 443
ciphers:
- HIGH
tls_ctx_options:
- OP_ALL
- OP_NO_SSLv3
- OP_CIPHER_SERVER_PREFERENCE
- OP_ENABLE_MIDDLEBOX_COMPAT
- OP_NO_COMPRESSION
- 4 # OP_LEGACY_SERVER_CONNECT
delegate_to: localhost
run_once: true
register: legacy_cert
'''

import atexit
import base64
import traceback
import ssl

from os.path import isfile
from socket import create_connection, setdefaulttimeout, socket
from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_REQUIRED

from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.common.text.converters import to_bytes
from ansible.module_utils.common.text.converters import to_bytes, to_native
from ansible.module_utils.six import string_types

from ansible_collections.community.crypto.plugins.module_utils.version import LooseVersion

Expand Down Expand Up @@ -285,6 +312,7 @@ def main():
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'),
),
)

Expand All @@ -298,6 +326,7 @@ def main():
start_tls_server_type = module.params.get('starttls')
ciphers = module.params.get('ciphers')
asn1_base64 = module.params['asn1_base64']
tls_ctx_options = module.params.get('tls_ctx_options')
if asn1_base64 is None:
module.deprecate(
'The default value `false` for asn1_base64 is deprecated and will change to `true` in '
Expand Down Expand Up @@ -346,6 +375,9 @@ def main():
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)
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)
try:
# Note: get_server_certificate does not support SNI!
cert = get_server_certificate((host, port), ca_certs=ca_cert)
Expand Down Expand Up @@ -381,6 +413,39 @@ def main():
ciphers_joined = ":".join(ciphers)
ctx.set_ciphers(ciphers_joined)

if tls_ctx_options is not None:
# Clear default ctx options
ctx.options = 0

# For each item in the tls_ctx_options list
for tls_ctx_option in tls_ctx_options:
# If the item is a string_type
if isinstance(tls_ctx_option, string_types):
# Convert tls_ctx_option to a native string
tls_ctx_option_str = to_native(tls_ctx_option)
# Get the tls_ctx_option_str attribute from ssl
tls_ctx_option_attr = getattr(ssl, tls_ctx_option_str)
dlehrman marked this conversation as resolved.
Show resolved Hide resolved
# If tls_ctx_option_attr is an integer
if isinstance(tls_ctx_option_attr, int):
# Set tls_ctx_option_int to the attribute value
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))
# 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")
dlehrman marked this conversation as resolved.
Show resolved Hide resolved

try:
# Add the int value of the item to ctx options
ctx.options |= tls_ctx_option_int
except Exception as e:
module.fail_json(msg="Failed to add {0} to CTX options".format(tls_ctx_option_str or tls_ctx_option_int))

cert = ctx.wrap_socket(sock, server_hostname=server_name or host).getpeercert(True)
cert = DER_cert_to_PEM_cert(cert)
except Exception as e:
Expand Down