Add SNI support to module get_certificates (#84)

* get_certificate - Add support of SNI

For python versions supporting `create_default_context` support SNI by using low-level
SSLContext.wrap_socket().getpeercert().

Add also more information in the error message

fixes #69

* Make sure default CA certificates are not loaded when ca_cert is specified.

* Refactor to combine common code.

* Update changelogs/fragments/get_certificate-add_support_for_SNI.yml

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
This commit is contained in:
Baptiste Mille-Mathias
2020-07-13 18:05:58 +02:00
committed by GitHub
parent c43d7c8725
commit 0786e93bb9
2 changed files with 39 additions and 24 deletions

View File

@@ -0,0 +1,4 @@
minor_changes:
- get_certificate - add support for SNI (https://github.com/ansible-collections/community.crypto/issues/69).
bugfixes:
- get_certificate - fix ``ca_cert`` option handling when ``proxy_host`` is used (https://github.com/ansible-collections/community.crypto/pull/84).

View File

@@ -18,6 +18,7 @@ description:
library. By default, it tries to detect which one is available. This can be library. By default, it tries to detect which one is available. This can be
overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL overridden with the I(select_crypto_backend) option. Please note that the PyOpenSSL
backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0." backend was deprecated in Ansible 2.9 and will be removed in community.crypto 2.0.0."
- Support SNI only with python >= 2.7
options: options:
host: host:
description: description:
@@ -154,8 +155,8 @@ import traceback
from distutils.version import LooseVersion from distutils.version import LooseVersion
from os.path import isfile from os.path import isfile
from socket import setdefaulttimeout, socket from socket import create_connection, setdefaulttimeout, socket
from ssl import get_server_certificate, DER_cert_to_PEM_cert, CERT_NONE, CERT_OPTIONAL 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.basic import AnsibleModule, missing_required_lib
from ansible.module_utils._text import to_bytes from ansible.module_utils._text import to_bytes
@@ -263,37 +264,47 @@ def main():
if not isfile(ca_cert): if not isfile(ca_cert):
module.fail_json(msg="ca_cert file does not exist") module.fail_json(msg="ca_cert file does not exist")
if proxy_host: if not HAS_CREATE_DEFAULT_CONTEXT:
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.', 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) exception=CREATE_DEFAULT_CONTEXT_IMP_ERR)
try: try:
connect = "CONNECT %s:%s HTTP/1.0\r\n\r\n" % (host, port) # Note: get_server_certificate does not support SNI!
sock = socket() cert = get_server_certificate((host, port), ca_certs=ca_cert)
atexit.register(sock.close) except Exception as e:
sock.connect((proxy_host, proxy_port)) module.fail_json(msg="Failed to get cert from {0}:{1}, error: {2}".format(host, port, e))
sock.send(connect.encode()) else:
sock.recv(8192) # Python >= 2.7.9
try:
ctx = create_default_context() if proxy_host:
ctx.check_hostname = False connect = "CONNECT %s:%s HTTP/1.0\r\n\r\n" % (host, port)
ctx.verify_mode = CERT_NONE sock = socket()
atexit.register(sock.close)
sock.connect((proxy_host, proxy_port))
sock.send(connect.encode())
sock.recv(8192)
else:
sock = create_connection((host, port))
atexit.register(sock.close)
if ca_cert: if ca_cert:
ctx.verify_mode = CERT_OPTIONAL ctx = create_default_context(cafile=ca_cert)
ctx.load_verify_locations(cafile=ca_cert) ctx.check_hostname = False
ctx.verify_mode = CERT_REQUIRED
else:
ctx = create_default_context()
ctx.check_hostname = False
ctx.verify_mode = CERT_NONE
cert = ctx.wrap_socket(sock, server_hostname=host).getpeercert(True) cert = ctx.wrap_socket(sock, server_hostname=host).getpeercert(True)
cert = DER_cert_to_PEM_cert(cert) cert = DER_cert_to_PEM_cert(cert)
except Exception as e: except Exception as e:
module.fail_json(msg="Failed to get cert from port with error: {0}".format(e)) if proxy_host:
module.fail_json(msg="Failed to get cert via proxy {0}:{1} from {2}:{3}, error: {4}".format(
else: proxy_host, proxy_port, host, port, e))
try: else:
cert = get_server_certificate((host, port), ca_certs=ca_cert) module.fail_json(msg="Failed to get cert from {0}:{1}, error: {2}".format(host, port, e))
except Exception as e:
module.fail_json(msg="Failed to get cert from port with error: {0}".format(e))
result['cert'] = cert result['cert'] = cert