Add support for GSSAPI/Kerberos to urls.py (#72113)

* Add support for GSSAPI/Kerberos to urls.py

* Test out changes with the latest test container

* Get remote hosts working

* Fix up httptester_krb5_password reader

* Fix tests for opensuse and macOS

* Hopefully last lot of testing changes

* Dont do CBT on macOS

* Fixes from review
This commit is contained in:
Jordan Borean 2020-10-13 14:16:07 +10:00 committed by GitHub
parent c4acd41d6e
commit caba47dd3f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 1028 additions and 11 deletions

View file

@ -0,0 +1,3 @@
minor_changes:
- Added support for GSSAPI/Kerberos authentication with ``urls.py`` that is used by ``uri`` and ``get_url``.
- Added support for specify custom credentials for GSSAPI authentication.

View file

@ -17,6 +17,7 @@ HTTP Testing endpoint which provides the following capabilities:
* nginx * nginx
* SSL * SSL
* SNI * SNI
* Negotiate Authentication
Source files can be found in the `http-test-container <https://github.com/ansible/http-test-container>`_ repository. Source files can be found in the `http-test-container <https://github.com/ansible/http-test-container>`_ repository.

View file

@ -74,17 +74,17 @@ import ansible.module_utils.six.moves.urllib.error as urllib_error
from ansible.module_utils.common.collections import Mapping from ansible.module_utils.common.collections import Mapping
from ansible.module_utils.six import PY3, string_types from ansible.module_utils.six import PY3, string_types
from ansible.module_utils.six.moves import cStringIO from ansible.module_utils.six.moves import cStringIO
from ansible.module_utils.basic import get_distribution from ansible.module_utils.basic import get_distribution, missing_required_lib
from ansible.module_utils._text import to_bytes, to_native, to_text from ansible.module_utils._text import to_bytes, to_native, to_text
try: try:
# python3 # python3
import urllib.request as urllib_request import urllib.request as urllib_request
from urllib.request import AbstractHTTPHandler from urllib.request import AbstractHTTPHandler, BaseHandler
except ImportError: except ImportError:
# python2 # python2
import urllib2 as urllib_request import urllib2 as urllib_request
from urllib2 import AbstractHTTPHandler from urllib2 import AbstractHTTPHandler, BaseHandler
urllib_request.HTTPRedirectHandler.http_error_308 = urllib_request.HTTPRedirectHandler.http_error_307 urllib_request.HTTPRedirectHandler.http_error_308 = urllib_request.HTTPRedirectHandler.http_error_307
@ -171,13 +171,105 @@ except ImportError:
except ImportError: except ImportError:
HAS_MATCH_HOSTNAME = False HAS_MATCH_HOSTNAME = False
HAS_CRYPTOGRAPHY = True
try:
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import UnsupportedAlgorithm
except ImportError:
HAS_CRYPTOGRAPHY = False
# Old import for GSSAPI authentication, this is not used in urls.py but kept for backwards compatibility.
try: try:
import urllib_gssapi import urllib_gssapi
HAS_GSSAPI = True HAS_GSSAPI = True
except ImportError: except ImportError:
HAS_GSSAPI = False HAS_GSSAPI = False
GSSAPI_IMP_ERR = None
try:
import gssapi
class HTTPGSSAPIAuthHandler(BaseHandler):
""" Handles Negotiate/Kerberos support through the gssapi library. """
AUTH_HEADER_PATTERN = re.compile(r'(?:.*)\s*(Negotiate|Kerberos)\s*([^,]*),?', re.I)
handler_order = 480 # Handle before Digest authentication
def __init__(self, username=None, password=None):
self.username = username
self.password = password
self._context = None
def get_auth_value(self, headers):
auth_match = self.AUTH_HEADER_PATTERN.search(headers.get('www-authenticate', ''))
if auth_match:
return auth_match.group(1), base64.b64decode(auth_match.group(2))
def http_error_401(self, req, fp, code, msg, headers):
# If we've already attempted the auth and we've reached this again then there was a failure.
if self._context:
return
parsed = generic_urlparse(urlparse(req.get_full_url()))
auth_header = self.get_auth_value(headers)
if not auth_header:
return
auth_protocol, in_token = auth_header
username = None
if self.username:
username = gssapi.Name(self.username, name_type=gssapi.NameType.user)
if username and self.password:
if not hasattr(gssapi.raw, 'acquire_cred_with_password'):
raise NotImplementedError("Platform GSSAPI library does not support "
"gss_acquire_cred_with_password, cannot acquire GSSAPI credential with "
"explicit username and password.")
b_password = to_bytes(self.password, errors='surrogate_or_strict')
cred = gssapi.raw.acquire_cred_with_password(username, b_password, usage='initiate').creds
else:
cred = gssapi.Credentials(name=username, usage='initiate')
# Get the peer certificate for the channel binding token if possible (HTTPS). A bug on macOS causes the
# authentication to fail when the CBT is present. Just skip that platform.
cbt = None
cert = getpeercert(fp, True)
if cert and platform.system() != 'Darwin':
cert_hash = get_channel_binding_cert_hash(cert)
if cert_hash:
cbt = gssapi.raw.ChannelBindings(application_data=b"tls-server-end-point:" + cert_hash)
# TODO: We could add another option that is set to include the port in the SPN if desired in the future.
target = gssapi.Name("HTTP@%s" % parsed['hostname'], gssapi.NameType.hostbased_service)
self._context = gssapi.SecurityContext(usage="initiate", name=target, creds=cred, channel_bindings=cbt)
resp = None
while not self._context.complete:
out_token = self._context.step(in_token)
if not out_token:
break
auth_header = '%s %s' % (auth_protocol, to_native(base64.b64encode(out_token)))
req.add_unredirected_header('Authorization', auth_header)
resp = self.parent.open(req)
# The response could contain a token that the client uses to validate the server
auth_header = self.get_auth_value(resp.headers)
if not auth_header:
break
in_token = auth_header[1]
return resp
except ImportError:
GSSAPI_IMP_ERR = traceback.format_exc()
HTTPGSSAPIAuthHandler = None
if not HAS_MATCH_HOSTNAME: if not HAS_MATCH_HOSTNAME:
# The following block of code is under the terms and conditions of the # The following block of code is under the terms and conditions of the
# Python Software Foundation License # Python Software Foundation License
@ -408,6 +500,13 @@ class NoSSLError(SSLValidationError):
pass pass
class MissingModuleError(Exception):
"""Failed to import 3rd party module required by the caller"""
def __init__(self, message, import_traceback):
super(MissingModuleError, self).__init__(message)
self.import_traceback = import_traceback
# Some environments (Google Compute Engine's CoreOS deploys) do not compile # Some environments (Google Compute Engine's CoreOS deploys) do not compile
# against openssl and thus do not have any HTTPS support. # against openssl and thus do not have any HTTPS support.
CustomHTTPSConnection = None CustomHTTPSConnection = None
@ -1032,6 +1131,43 @@ def maybe_add_ssl_handler(url, validate_certs, ca_path=None):
return SSLValidationHandler(parsed.hostname, parsed.port or 443, ca_path=ca_path) return SSLValidationHandler(parsed.hostname, parsed.port or 443, ca_path=ca_path)
def getpeercert(response, binary_form=False):
""" Attempt to get the peer certificate of the response from urlopen. """
# The response from urllib2.open() is different across Python 2 and 3
if PY3:
socket = response.fp.raw._sock
else:
socket = response.fp._sock.fp._sock
try:
return socket.getpeercert(binary_form)
except AttributeError:
pass # Not HTTPS
def get_channel_binding_cert_hash(certificate_der):
""" Gets the channel binding app data for a TLS connection using the peer cert. """
if not HAS_CRYPTOGRAPHY:
return
# Logic documented in RFC 5929 section 4 https://tools.ietf.org/html/rfc5929#section-4
cert = x509.load_der_x509_certificate(certificate_der, default_backend())
hash_algorithm = None
try:
hash_algorithm = cert.signature_hash_algorithm
except UnsupportedAlgorithm:
pass
# If the signature hash algorithm is unknown/unsupported or md5/sha1 we must use SHA256.
if not hash_algorithm or hash_algorithm.name in ['md5', 'sha1']:
hash_algorithm = hashes.SHA256()
digest = hashes.Hash(hash_algorithm, default_backend())
digest.update(certificate_der)
return digest.finalize()
def rfc2822_date_string(timetuple, zone='-0000'): def rfc2822_date_string(timetuple, zone='-0000'):
"""Accepts a timetuple and optional zone which defaults to ``-0000`` """Accepts a timetuple and optional zone which defaults to ``-0000``
and returns a date string as specified by RFC 2822, e.g.: and returns a date string as specified by RFC 2822, e.g.:
@ -1176,15 +1312,13 @@ class Request:
ssl_handler = maybe_add_ssl_handler(url, validate_certs, ca_path=ca_path) ssl_handler = maybe_add_ssl_handler(url, validate_certs, ca_path=ca_path)
if ssl_handler and not HAS_SSLCONTEXT: if ssl_handler and not HAS_SSLCONTEXT:
handlers.append(ssl_handler) handlers.append(ssl_handler)
if HAS_GSSAPI and use_gssapi:
handlers.append(urllib_gssapi.HTTPSPNEGOAuthHandler())
parsed = generic_urlparse(urlparse(url)) parsed = generic_urlparse(urlparse(url))
if parsed.scheme != 'ftp': if parsed.scheme != 'ftp':
username = url_username username = url_username
password = url_password
if username: if username:
password = url_password
netloc = parsed.netloc netloc = parsed.netloc
elif '@' in parsed.netloc: elif '@' in parsed.netloc:
credentials, netloc = parsed.netloc.split('@', 1) credentials, netloc = parsed.netloc.split('@', 1)
@ -1200,7 +1334,15 @@ class Request:
# reconstruct url without credentials # reconstruct url without credentials
url = urlunparse(parsed_list) url = urlunparse(parsed_list)
if username and not force_basic_auth: if use_gssapi:
if HTTPGSSAPIAuthHandler:
handlers.append(HTTPGSSAPIAuthHandler(username, password))
else:
imp_err_msg = missing_required_lib('gssapi', reason='for use_gssapi=True',
url='https://pypi.org/project/gssapi/')
raise MissingModuleError(imp_err_msg, import_traceback=GSSAPI_IMP_ERR)
elif username and not force_basic_auth:
passman = urllib_request.HTTPPasswordMgrWithDefaultRealm() passman = urllib_request.HTTPPasswordMgrWithDefaultRealm()
# this creates a password manager # this creates a password manager
@ -1543,6 +1685,7 @@ def url_argument_spec():
force_basic_auth=dict(type='bool', default=False), force_basic_auth=dict(type='bool', default=False),
client_cert=dict(type='path'), client_cert=dict(type='path'),
client_key=dict(type='path'), client_key=dict(type='path'),
use_gssapi=dict(type='bool', default=False),
) )
@ -1603,6 +1746,7 @@ def fetch_url(module, url, data=None, headers=None, method=None,
client_cert = module.params.get('client_cert') client_cert = module.params.get('client_cert')
client_key = module.params.get('client_key') client_key = module.params.get('client_key')
use_gssapi = module.params.get('use_gssapi', use_gssapi)
if not isinstance(cookies, cookiejar.CookieJar): if not isinstance(cookies, cookiejar.CookieJar):
cookies = cookiejar.LWPCookieJar() cookies = cookiejar.LWPCookieJar()
@ -1655,6 +1799,8 @@ def fetch_url(module, url, data=None, headers=None, method=None,
module.fail_json(msg='%s' % to_native(e), **info) module.fail_json(msg='%s' % to_native(e), **info)
except (ConnectionError, ValueError) as e: except (ConnectionError, ValueError) as e:
module.fail_json(msg=to_native(e), **info) module.fail_json(msg=to_native(e), **info)
except MissingModuleError as e:
module.fail_json(msg=to_text(e), exception=e.import_traceback)
except urllib_error.HTTPError as e: except urllib_error.HTTPError as e:
try: try:
body = e.read() body = e.read()

View file

@ -166,6 +166,17 @@ options:
- Header to identify as, generally appears in web server logs. - Header to identify as, generally appears in web server logs.
type: str type: str
default: ansible-httpget default: ansible-httpget
use_gssapi:
description:
- Use GSSAPI to perform the authentication, typically this is for Kerberos or Kerberos through Negotiate
authentication.
- Requires the Python library L(gssapi,https://github.com/pythongssapi/python-gssapi) to be installed.
- Credentials for GSSAPI can be specified with I(url_username)/I(url_password) or with the GSSAPI env var
C(KRB5CCNAME) that specified a custom Kerberos credential cache.
- NTLM authentication is C(not) supported even if the GSSAPI mech for NTLM has been installed.
type: bool
default: no
version_added: '2.11'
# informational: requirements for nodes # informational: requirements for nodes
extends_documentation_fragment: extends_documentation_fragment:
- files - files

View file

@ -176,6 +176,17 @@ options:
- Header to identify as, generally appears in web server logs. - Header to identify as, generally appears in web server logs.
type: str type: str
default: ansible-httpget default: ansible-httpget
use_gssapi:
description:
- Use GSSAPI to perform the authentication, typically this is for Kerberos or Kerberos through Negotiate
authentication.
- Requires the Python library L(gssapi,https://github.com/pythongssapi/python-gssapi) to be installed.
- Credentials for GSSAPI can be specified with I(url_username)/I(url_password) or with the GSSAPI env var
C(KRB5CCNAME) that specified a custom Kerberos credential cache.
- NTLM authentication is C(not) supported even if the GSSAPI mech for NTLM has been installed.
type: bool
default: no
version_added: '2.11'
notes: notes:
- The dependency on httplib2 was removed in Ansible 2.1. - The dependency on httplib2 was removed in Ansible 2.1.
- The module returns all the HTTP headers in lower-case. - The module returns all the HTTP headers in lower-case.

View file

@ -63,4 +63,15 @@ options:
- PEM formatted file that contains your private key to be used for SSL client authentication. - PEM formatted file that contains your private key to be used for SSL client authentication.
- If C(client_cert) contains both the certificate and key, this option is not required. - If C(client_cert) contains both the certificate and key, this option is not required.
type: path type: path
use_gssapi:
description:
- Use GSSAPI to perform the authentication, typically this is for Kerberos or Kerberos through Negotiate
authentication.
- Requires the Python library L(gssapi,https://github.com/pythongssapi/python-gssapi) to be installed.
- Credentials for GSSAPI can be specified with I(url_username)/I(url_password) or with the GSSAPI env var
C(KRB5CCNAME) that specified a custom Kerberos credential cache.
- NTLM authentication is C(not) supported even if the GSSAPI mech for NTLM has been installed.
type: bool
default: no
version_added: '2.11'
''' '''

View file

@ -99,7 +99,9 @@ options:
- section: url_lookup - section: url_lookup
key: follow_redirects key: follow_redirects
use_gssapi: use_gssapi:
description: Use GSSAPI handler of requests description:
- Use GSSAPI handler of requests
- As of Ansible 2.11, GSSAPI credentials can be specified with I(username) and I(password).
type: boolean type: boolean
version_added: "2.10" version_added: "2.10"
default: False default: False

View file

@ -534,3 +534,12 @@
that: that:
- '(result.content | b64decode) == "ansible.http.tests:SUCCESS"' - '(result.content | b64decode) == "ansible.http.tests:SUCCESS"'
when: has_httptester when: has_httptester
- name: Test use_gssapi=True
include_tasks:
file: use_gssapi.yml
apply:
environment:
KRB5_CONFIG: '{{ krb5_config }}'
KRB5CCNAME: FILE:{{ remote_tmp_dir }}/krb5.cc
when: krb5_config is defined

View file

@ -0,0 +1,60 @@
- name: Skip explicit auth tests on FreeBSD as Heimdal there does not have gss_acquire_cred_with_password
when: ansible_facts.os_family != 'FreeBSD'
block:
- name: test Negotiate auth over HTTP with explicit credentials
get_url:
url: http://{{ httpbin_host }}/gssapi
dest: '{{ remote_tmp_dir }}/gssapi_explicit.txt'
use_gssapi: yes
url_username: '{{ krb5_username }}'
url_password: '{{ krb5_password }}'
register: http_explicit
- name: get result of test Negotiate auth over HTTP with explicit credentials
slurp:
path: '{{ remote_tmp_dir }}/gssapi_explicit.txt'
register: http_explicit_actual
- name: assert test Negotiate auth with implicit credentials
assert:
that:
- http_explicit.status_code == 200
- http_explicit_actual.content | b64decode | trim == 'Microsoft Rulz'
- name: FreeBSD - verify it fails with explicit credential
get_url:
url: http://{{ httpbin_host }}/gssapi
dest: '{{ remote_tmp_dir }}/gssapi_explicit.txt'
use_gssapi: yes
url_username: '{{ krb5_username }}'
url_password: '{{ krb5_password }}'
register: explicit_failure
when: ansible_facts.os_family == 'FreeBSD'
failed_when:
- '"Platform GSSAPI library does not support gss_acquire_cred_with_password, cannot acquire GSSAPI credential with explicit username and password" not in explicit_failure.msg'
- name: skip tests on macOS, I cannot seem to get it to read a credential from a custom ccache
when: ansible_facts.distribution != 'MacOSX'
block:
- name: get Kerberos ticket for implicit auth tests
httptester_kinit:
username: '{{ krb5_username }}'
password: '{{ krb5_password }}'
- name: test Negotiate auth over HTTPS with implicit credentials
get_url:
url: https://{{ httpbin_host }}/gssapi
dest: '{{ remote_tmp_dir }}/gssapi_implicit.txt'
use_gssapi: yes
register: https_implicit
- name: get result of test Negotiate auth over HTTPS with implicit credentials
slurp:
path: '{{ remote_tmp_dir }}/gssapi_implicit.txt'
register: https_implicit_actual
- name: assert test Negotiate auth with implicit credentials
assert:
that:
- https_implicit.status_code == 200
- https_implicit_actual.content | b64decode | trim == 'Microsoft Rulz'

View file

@ -0,0 +1,2 @@
shippable/posix/group1
needs/httptester

View file

@ -0,0 +1,98 @@
#!/usr/bin/python
# Copyright: (c) 2020, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: test_perrcert
short_description: Test getting the peer certificate of a HTTP response
description: Test getting the peer certificate of a HTTP response.
options:
url:
description: The endpoint to get the peer cert for
required: true
type: str
author:
- Ansible Project
'''
EXAMPLES = r'''
#
'''
RETURN = r'''
#
'''
import base64
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_text
from ansible.module_utils.urls import getpeercert, Request
def get_x509_shorthand(name, value):
prefix = {
'countryName': 'C',
'stateOrProvinceName': 'ST',
'localityName': 'L',
'organizationName': 'O',
'commonName': 'CN',
'organizationalUnitName': 'OU',
}[name]
return '%s=%s' % (prefix, value)
def main():
module_args = dict(
url=dict(type='str', required=True),
)
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
)
result = {
'changed': False,
'cert': None,
'raw_cert': None,
}
req = Request().get(module.params['url'])
try:
cert = getpeercert(req)
b_cert = getpeercert(req, binary_form=True)
finally:
req.close()
if cert:
processed_cert = {
'issuer': '',
'not_after': cert.get('notAfter', None),
'not_before': cert.get('notBefore', None),
'serial_number': cert.get('serialNumber', None),
'subject': '',
'version': cert.get('version', None),
}
for field in ['issuer', 'subject']:
field_values = []
for x509_part in cert.get(field, []):
field_values.append(get_x509_shorthand(x509_part[0][0], x509_part[0][1]))
processed_cert[field] = ",".join(field_values)
result['cert'] = processed_cert
if b_cert:
result['raw_cert'] = to_text(base64.b64encode(b_cert))
module.exit_json(**result)
if __name__ == '__main__':
main()

View file

@ -0,0 +1,3 @@
dependencies:
- prepare_http_tests
- setup_remote_tmp_dir

View file

@ -0,0 +1,32 @@
- name: get peercert for HTTP connection
test_peercert:
url: http://{{ httpbin_host }}/get
register: cert_http
- name: assert get peercert for HTTP connection
assert:
that:
- cert_http.raw_cert == None
- name: get peercert for HTTPS connection
test_peercert:
url: https://{{ httpbin_host }}/get
register: cert_https
# Alpine does not have openssl, just make sure the text was actually set instead
- name: check if openssl is installed
command: which openssl
ignore_errors: yes
register: openssl
- name: get actual certificate from endpoint
shell: echo | openssl s_client -connect {{ httpbin_host }}:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p'
register: cert_https_actual
changed_when: no
when: openssl is successful
- name: assert get peercert for HTTPS connection
assert:
that:
- cert_https.raw_cert != None
- openssl is failed or cert_https.raw_cert == cert_https_actual.stdout_lines[1:-1] | join("")

View file

@ -0,0 +1,4 @@
- name: Remove python gssapi
pip:
name: gssapi
state: absent

View file

@ -0,0 +1,134 @@
#!/usr/bin/python
# Copyright: (c) 2020, Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
DOCUMENTATION = r'''
---
module: httptester_kinit
short_description: Get Kerberos ticket
description: Get Kerberos ticket using kinit non-interactively.
options:
username:
description: The username to get the ticket for.
required: true
type: str
password:
description: The password for I(username).
required; true
type: str
author:
- Ansible Project
'''
EXAMPLES = r'''
#
'''
RETURN = r'''
#
'''
import contextlib
import errno
import os
import subprocess
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_bytes, to_text
try:
import configparser
except ImportError:
import ConfigParser as configparser
@contextlib.contextmanager
def env_path(name, value, default_value):
""" Adds a value to a PATH-like env var and preserve the existing value if present. """
orig_value = os.environ.get(name, None)
os.environ[name] = '%s:%s' % (value, orig_value or default_value)
try:
yield
finally:
if orig_value:
os.environ[name] = orig_value
else:
del os.environ[name]
@contextlib.contextmanager
def krb5_conf(module, config):
""" Runs with a custom krb5.conf file that extends the existing config if present. """
if config:
ini_config = configparser.ConfigParser()
for section, entries in config.items():
ini_config.add_section(section)
for key, value in entries.items():
ini_config.set(section, key, value)
config_path = os.path.join(module.tmpdir, 'krb5.conf')
with open(config_path, mode='wt') as config_fd:
ini_config.write(config_fd)
with env_path('KRB5_CONFIG', config_path, '/etc/krb5.conf'):
yield
else:
yield
def main():
module_args = dict(
username=dict(type='str', required=True),
password=dict(type='str', required=True, no_log=True),
)
module = AnsibleModule(
argument_spec=module_args,
required_together=[('username', 'password')],
)
# Debugging purposes, get the Kerberos version. On platforms like OpenSUSE this may not be on the PATH.
try:
process = subprocess.Popen(['krb5-config', '--version'], stdout=subprocess.PIPE)
stdout, stderr = process.communicate()
version = to_text(stdout)
except OSError as e:
if e.errno != errno.ENOENT:
raise
version = 'Unknown (no krb5-config)'
# Heimdal has a few quirks that we want to paper over in this module
# 1. KRB5_TRACE does not work in any released version (<=7.7), we need to use a custom krb5.config to enable it
# 2. When reading the password it reads from the pty not stdin by default causing an issue with subprocess. We
# can control that behaviour with '--password-file=STDIN'
is_heimdal = os.uname()[0] in ['Darwin', 'FreeBSD']
kinit_args = ['kinit']
config = {}
if is_heimdal:
kinit_args.append('--password-file=STDIN')
config['logging'] = {'krb5': 'FILE:/dev/stdout'}
kinit_args.append(to_text(module.params['username'], errors='surrogate_or_strict'))
with krb5_conf(module, config):
# Weirdly setting KRB5_CONFIG in the modules environment block does not work unless we pass it in explicitly.
# Take a copy of the existing environment to make sure the process has the same env vars as ours. Also set
# KRB5_TRACE to output and debug logs helping to identify problems when calling kinit with MIT.
kinit_env = os.environ.copy()
kinit_env['KRB5_TRACE'] = '/dev/stdout'
process = subprocess.Popen(kinit_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=kinit_env)
stdout, stderr = process.communicate(to_bytes(module.params['password'], errors='surrogate_or_strict') + b'\n')
rc = process.returncode
module.exit_json(changed=True, stdout=to_text(stdout), stderr=to_text(stderr), rc=rc, version=version)
if __name__ == '__main__':
main()

View file

@ -1,2 +1,3 @@
dependencies: dependencies:
- setup_remote_tmp_dir - setup_remote_tmp_dir
- setup_remote_constraints

View file

@ -0,0 +1,61 @@
- set_fact:
krb5_config: '{{ remote_tmp_dir }}/krb5.conf'
krb5_realm: '{{ httpbin_host.split(".")[1:] | join(".") | upper }}'
krb5_provider: '{{ (ansible_facts.os_family == "FreeBSD" or ansible_facts.distribution == "MacOSX") | ternary("Heimdal", "MIT") }}'
- set_fact:
krb5_username: admin@{{ krb5_realm }}
- name: Create krb5.conf file
template:
src: krb5.conf.j2
dest: '{{ krb5_config }}'
- name: Include distribution specific variables
include_vars: '{{ lookup("first_found", params) }}'
vars:
params:
files:
- '{{ ansible_facts.distribution }}-{{ ansible_facts.distribution_major_version }}.yml'
- '{{ ansible_facts.os_family }}-{{ ansible_facts.distribution_major_version }}.yml'
- '{{ ansible_facts.distribution }}.yml'
- '{{ ansible_facts.os_family }}.yml'
- default.yml
paths:
- '{{ role_path }}/vars'
- name: Install Kerberos sytem packages
package:
name: '{{ krb5_packages }}'
state: present
when: ansible_facts.distribution not in ['Alpine', 'MacOSX']
# apk isn't available on ansible-base so just call command
- name: Alpine - Install Kerberos system packages
command: apk add {{ krb5_packages | join(' ') }}
when: ansible_facts.distribution == 'Alpine'
- name: Install python gssapi
pip:
name:
- gssapi
- importlib ; python_version < '2.7'
state: present
extra_args: '-c {{ remote_constraints }}'
environment:
# Need this custom path for OpenSUSE as krb5-config is placed there
PATH: '{{ ansible_facts.env.PATH }}:/usr/lib/mit/bin'
notify: Remove python gssapi
- name: test the environment to make sure Kerberos is working properly
httptester_kinit:
username: '{{ krb5_username }}'
password: '{{ krb5_password }}'
environment:
KRB5_CONFIG: '{{ krb5_config }}'
KRB5CCNAME: FILE:{{ remote_tmp_dir }}/krb5.cc
- name: remove test credential cache
file:
path: '{{ remote_tmp_dir }}/krb5.cc'
state: absent

View file

@ -22,3 +22,13 @@
- has_httptester|bool - has_httptester|bool
# skip the setup if running on Windows Server 2008 as httptester is not available # skip the setup if running on Windows Server 2008 as httptester is not available
- ansible_os_family != 'Windows' or (ansible_os_family == 'Windows' and not ansible_distribution_version.startswith("6.0.")) - ansible_os_family != 'Windows' or (ansible_os_family == 'Windows' and not ansible_distribution_version.startswith("6.0."))
- set_fact:
krb5_password: "{{ lookup('env', 'KRB5_PASSWORD') }}"
- name: setup Kerberos client
include_tasks: kerberos.yml
when:
- has_httptester|bool
- ansible_os_family != 'Windows'
- krb5_password != ''

View file

@ -0,0 +1,25 @@
[libdefaults]
default_realm = {{ krb5_realm | upper }}
dns_lookup_realm = false
dns_lookup_kdc = false
rdns = false
[realms]
{{ krb5_realm | upper }} = {
{% if krb5_provider == 'Heimdal' %}
{# Heimdal seems to only use UDP unless TCP is explicitly set and we must use TCP as the SSH tunnel only supports TCP. #}
{# The hostname doesn't seem to work when using the alias, just use localhost as that works. #}
kdc = tcp/127.0.0.1
admin_server = tcp/127.0.0.1
{% else %}
kdc = {{ httpbin_host }}
admin_server = {{ httpbin_host }}
{% endif %}
}
[domain_realm]
.{{ krb5_realm | lower }} = {{ krb5_realm | upper }}
{{ krb5_realm | lower }} = {{ krb5_realm | upper }}
[logging]
krb5 = FILE:/dev/stdout

View file

@ -0,0 +1,3 @@
krb5_packages:
- krb5
- krb5-dev

View file

@ -0,0 +1,3 @@
krb5_packages:
- krb5-user
- libkrb5-dev

View file

@ -0,0 +1,2 @@
krb5_packages:
- heimdal

View file

@ -0,0 +1,3 @@
krb5_packages:
- krb5-client
- krb5-devel

View file

@ -0,0 +1,3 @@
krb5_packages:
- krb5-devel
- krb5-workstation

View file

@ -598,3 +598,12 @@
- name: Check return-content - name: Check return-content
import_tasks: return-content.yml import_tasks: return-content.yml
- name: Test use_gssapi=True
include_tasks:
file: use_gssapi.yml
apply:
environment:
KRB5_CONFIG: '{{ krb5_config }}'
KRB5CCNAME: FILE:{{ remote_tmp_dir }}/krb5.cc
when: krb5_config is defined

View file

@ -0,0 +1,76 @@
- name: test that endpoint offers Negotiate auth
uri:
url: http://{{ httpbin_host }}/gssapi
status_code: 401
register: no_auth_failure
failed_when: no_auth_failure.www_authenticate != 'Negotiate'
- name: Skip explicit auth tests on FreeBSD as Heimdal there does not have gss_acquire_cred_with_password
when: ansible_facts.os_family != 'FreeBSD'
block:
- name: test Negotiate auth over HTTP with explicit credentials
uri:
url: http://{{ httpbin_host }}/gssapi
use_gssapi: yes
url_username: '{{ krb5_username }}'
url_password: '{{ krb5_password }}'
return_content: yes
register: http_explicit
- name: test Negotiate auth over HTTPS with explicit credentials
uri:
url: https://{{ httpbin_host }}/gssapi
use_gssapi: yes
url_username: '{{ krb5_username }}'
url_password: '{{ krb5_password }}'
return_content: yes
register: https_explicit
- name: assert test Negotiate auth with implicit credentials
assert:
that:
- http_explicit.status == 200
- http_explicit.content | trim == 'Microsoft Rulz'
- https_explicit.status == 200
- https_explicit.content | trim == 'Microsoft Rulz'
- name: FreeBSD - verify it fails with explicit credential
uri:
url: https://{{ httpbin_host }}/gssapi
use_gssapi: yes
url_username: '{{ krb5_username }}'
url_password: '{{ krb5_password }}'
register: explicit_failure
when: ansible_facts.os_family == 'FreeBSD'
failed_when:
- '"Platform GSSAPI library does not support gss_acquire_cred_with_password, cannot acquire GSSAPI credential with explicit username and password" not in explicit_failure.msg'
- name: skip tests on macOS, I cannot seem to get it to read a credential from a custom ccache
when: ansible_facts.distribution != 'MacOSX'
block:
- name: get Kerberos ticket for implicit auth tests
httptester_kinit:
username: '{{ krb5_username }}'
password: '{{ krb5_password }}'
- name: test Negotiate auth over HTTP with implicit credentials
uri:
url: http://{{ httpbin_host }}/gssapi
use_gssapi: yes
return_content: yes
register: http_implicit
- name: test Negotiate auth over HTTPS with implicit credentials
uri:
url: https://{{ httpbin_host }}/gssapi
use_gssapi: yes
return_content: yes
register: https_implicit
- name: assert test Negotiate auth with implicit credentials
assert:
that:
- http_implicit.status == 200
- http_implicit.content | trim == 'Microsoft Rulz'
- https_implicit.status == 200
- https_implicit.content | trim == 'Microsoft Rulz'

View file

@ -43,6 +43,7 @@ botocore >= 1.10.0, < 1.14 ; python_version < '2.7' # adds support for the follo
botocore >= 1.10.0 ; python_version >= '2.7' # adds support for the following AWS services: secretsmanager, fms, and acm-pca botocore >= 1.10.0 ; python_version >= '2.7' # adds support for the following AWS services: secretsmanager, fms, and acm-pca
setuptools < 37 ; python_version == '2.6' # setuptools 37 and later require python 2.7 or later setuptools < 37 ; python_version == '2.6' # setuptools 37 and later require python 2.7 or later
setuptools < 45 ; python_version == '2.7' # setuptools 45 and later require python 3.5 or later setuptools < 45 ; python_version == '2.7' # setuptools 45 and later require python 3.5 or later
gssapi < 1.6.0 ; python_version <= '2.7' # gssapi 1.6.0 and later require python 3 or later
# freeze antsibull-changelog for consistent test results # freeze antsibull-changelog for consistent test results
antsibull-changelog == 0.7.0 antsibull-changelog == 0.7.0

View file

@ -1003,7 +1003,7 @@ def add_httptester_options(parser, argparse):
group.add_argument('--httptester', group.add_argument('--httptester',
metavar='IMAGE', metavar='IMAGE',
default='quay.io/ansible/http-test-container:1.1.0', default='quay.io/ansible/http-test-container:1.3.0',
help='docker image to use for the httptester container') help='docker image to use for the httptester container')
group.add_argument('--disable-httptester', group.add_argument('--disable-httptester',
@ -1016,6 +1016,9 @@ def add_httptester_options(parser, argparse):
action='store_true', action='store_true',
help=argparse.SUPPRESS) # internal use only help=argparse.SUPPRESS) # internal use only
parser.add_argument('--httptester-krb5-password',
help=argparse.SUPPRESS) # internal use only
def add_extra_docker_options(parser, integration=True): def add_extra_docker_options(parser, integration=True):
""" """

View file

@ -9,6 +9,7 @@ from . import types as t
from .util import ( from .util import (
find_python, find_python,
generate_password,
generate_pip_command, generate_pip_command,
ApplicationError, ApplicationError,
) )
@ -123,6 +124,8 @@ class EnvironmentConfig(CommonConfig):
self.inject_httptester = args.inject_httptester if 'inject_httptester' in args else False # type: bool self.inject_httptester = args.inject_httptester if 'inject_httptester' in args else False # type: bool
self.httptester = docker_qualify_image(args.httptester if 'httptester' in args else '') # type: str self.httptester = docker_qualify_image(args.httptester if 'httptester' in args else '') # type: str
krb5_password = args.httptester_krb5_password if 'httptester_krb5_password' in args else ''
self.httptester_krb5_password = krb5_password or generate_password() # type: str
if self.get_delegated_completion().get('httptester', 'enabled') == 'disabled': if self.get_delegated_completion().get('httptester', 'enabled') == 'disabled':
self.httptester = False self.httptester = False

View file

@ -309,7 +309,7 @@ def delegate_docker(args, exclude, require, integration_targets):
test_options += ['--volume', '%s:%s' % (docker_socket, docker_socket)] test_options += ['--volume', '%s:%s' % (docker_socket, docker_socket)]
if httptester_id: if httptester_id:
test_options += ['--env', 'HTTPTESTER=1'] test_options += ['--env', 'HTTPTESTER=1', '--env', 'KRB5_PASSWORD=%s' % args.httptester_krb5_password]
for host in HTTPTESTER_HOSTS: for host in HTTPTESTER_HOSTS:
test_options += ['--link', '%s:%s' % (httptester_id, host)] test_options += ['--link', '%s:%s' % (httptester_id, host)]
@ -462,7 +462,7 @@ def delegate_remote(args, exclude, require, integration_targets):
cmd = generate_command(args, python_interpreter, os.path.join(ansible_root, 'bin'), content_root, options, exclude, require) cmd = generate_command(args, python_interpreter, os.path.join(ansible_root, 'bin'), content_root, options, exclude, require)
if httptester_id: if httptester_id:
cmd += ['--inject-httptester'] cmd += ['--inject-httptester', '--httptester-krb5-password', args.httptester_krb5_password]
if isinstance(args, TestConfig): if isinstance(args, TestConfig):
if args.coverage and not args.coverage_label: if args.coverage and not args.coverage_label:

View file

@ -1247,10 +1247,18 @@ def start_httptester(args):
remote=8080, remote=8080,
container=80, container=80,
), ),
dict(
remote=8088,
container=88,
),
dict( dict(
remote=8443, remote=8443,
container=443, container=443,
), ),
dict(
remote=8749,
container=749,
),
] ]
container_id = get_docker_container_id() container_id = get_docker_container_id()
@ -1287,6 +1295,7 @@ def run_httptester(args, ports=None):
""" """
options = [ options = [
'--detach', '--detach',
'--env', 'KRB5_PASSWORD=%s' % args.httptester_krb5_password,
] ]
if ports: if ports:
@ -1331,7 +1340,9 @@ def inject_httptester(args):
rules = ''' rules = '''
rdr pass inet proto tcp from any to any port 80 -> 127.0.0.1 port 8080 rdr pass inet proto tcp from any to any port 80 -> 127.0.0.1 port 8080
rdr pass inet proto tcp from any to any port 88 -> 127.0.0.1 port 8088
rdr pass inet proto tcp from any to any port 443 -> 127.0.0.1 port 8443 rdr pass inet proto tcp from any to any port 443 -> 127.0.0.1 port 8443
rdr pass inet proto tcp from any to any port 749 -> 127.0.0.1 port 8749
''' '''
cmd = ['pfctl', '-ef', '-'] cmd = ['pfctl', '-ef', '-']
@ -1343,7 +1354,9 @@ rdr pass inet proto tcp from any to any port 443 -> 127.0.0.1 port 8443
elif iptables: elif iptables:
ports = [ ports = [
(80, 8080), (80, 8080),
(88, 8088),
(443, 8443), (443, 8443),
(749, 8749),
] ]
for src, dst in ports: for src, dst in ports:
@ -1406,6 +1419,7 @@ def integration_environment(args, target, test_dir, inventory_path, ansible_conf
if args.inject_httptester: if args.inject_httptester:
env.update(dict( env.update(dict(
HTTPTESTER='1', HTTPTESTER='1',
KRB5_PASSWORD=args.httptester_krb5_password,
)) ))
callback_plugins = ['junit'] + (env_config.callback_plugins or [] if env_config else []) callback_plugins = ['junit'] + (env_config.callback_plugins or [] if env_config else [])

View file

@ -364,6 +364,7 @@ def common_environment():
optional = ( optional = (
'HTTPTESTER', 'HTTPTESTER',
'KRB5_PASSWORD',
'LD_LIBRARY_PATH', 'LD_LIBRARY_PATH',
'SSH_AUTH_SOCK', 'SSH_AUTH_SOCK',
# MacOS High Sierra Compatibility # MacOS High Sierra Compatibility

View file

@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBjzCCATWgAwIBAgIQeNQTxkMgq4BF9tKogIGXUTAKBggqhkjOPQQ
DAjAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MDMxN1
oXDTE4MDUzMDA4MjMxN1owFTETMBEGA1UEAwwKU0VSVkVSMjAxNjBZM
BMGByqGSM49AgEGCCqGSM49AwEHA0IABDAfXTLOaC3ElgErlgk2tBlM
wf9XmGlGBw4vBtMJap1hAqbsdxFm6rhK3QU8PFFpv8Z/AtRG7ba3UwQ
prkssClejZzBlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBg
EFBQcDAgYIKwYBBQUHAwEwFQYDVR0RBA4wDIIKU0VSVkVSMjAxNjAdB
gNVHQ4EFgQUnFDE8824TYAiBeX4fghEEg33UgYwCgYIKoZIzj0EAwID
SAAwRQIhAK3rXA4/0i6nm/U7bi6y618Ci2Is8++M3tYIXnEsA7zSAiA
w2s6bJoI+D7Xaey0Hp0gkks9z55y976keIEI+n3qkzw==
-----END CERTIFICATE-----

View file

@ -0,0 +1,12 @@
-----BEGIN CERTIFICATE-----
MIIBjjCCATWgAwIBAgIQHVj2AGEwd6pOOSbcf0skQDAKBggqhkjOPQQ
DBDAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA3NTUzOV
oXDTE4MDUzMDA4MTUzOVowFTETMBEGA1UEAwwKU0VSVkVSMjAxNjBZM
BMGByqGSM49AgEGCCqGSM49AwEHA0IABL8d9S++MFpfzeH8B3vG/PjA
AWg8tGJVgsMw9nR+OfC9ltbTUwhB+yPk3JPcfW/bqsyeUgq4//LhaSp
lOWFNaNqjZzBlMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBg
EFBQcDAgYIKwYBBQUHAwEwFQYDVR0RBA4wDIIKU0VSVkVSMjAxNjAdB
gNVHQ4EFgQUKUkCgLlxoeai0EtQrZth1/BSc5kwCgYIKoZIzj0EAwQD
RwAwRAIgRrV7CLpDG7KueyFA3ZDced9dPOcv2Eydx/hgrfxYEcYCIBQ
D35JvzmqU05kSFV5eTvkhkaDObd7V55vokhm31+Li
-----END CERTIFICATE-----

View file

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDZDCCAhugAwIBAgIUbo9YpgkeniC5jRmOgFYX3mcVJn4wPgYJKoZIhvcNAQEK
MDGgDTALBglghkgBZQMEAgGhGjAYBgkqhkiG9w0BAQgwCwYJYIZIAWUDBAIBogQC
AgDeMBMxETAPBgNVBAMMCE9NSSBSb290MB4XDTIwMDkwNDE4NTMyNloXDTIxMDkw
NDE4NTMyNlowGDEWMBQGA1UEAwwNREMwMS5vbWkudGVzdDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBANN++3POgcKcPILMdHWIEPiVENtKoJQ8iqKKeL+/
j5oUULVuIn15H/RYMNFmStRIvj0dIL1JAq4W411wG2Tf/6niU2YSKPOAOtrVREef
gNvMZ06TYlC8UcGCLv4dBkU3q/FELV66lX9x6LcVwf2f8VWfDg4VNuwyg/eQUIgc
/yd5VV+1VXTf39QufVV+/hOtPptu+fBKOIuiuKm6FIqroqLri0Ysp6tWrSd7P6V4
6zT2yd17981vaEv5Zek2t39PoLYzJb3rvqQmumgFBIUQ1eMPLFCXX8YYYC/9ByK3
mdQaEnkD2eIOARLnojr2A228EgPpdM8phQkDzeWeYnhLiysCAwEAAaNJMEcwCQYD
VR0TBAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGAYDVR0R
BBEwD4INREMwMS5vbWkudGVzdDA+BgkqhkiG9w0BAQowMaANMAsGCWCGSAFlAwQC
AaEaMBgGCSqGSIb3DQEBCDALBglghkgBZQMEAgGiBAICAN4DggEBAA66cbtiysjq
sCaDiYyRWCS9af2DGxJ6NAyku2aX+xgmRQzUzFAN5TihcPau+zzpk2zQKHDVMhNx
ouhTkIe6mq9KegpUuesWwkJ5KEeuBT949tIru2wAtlSVDvDcau5T9pRI/RLlnsWg
0sWaUAh/ujL+VKk6AanC4MRV69llwJcAVxlS/tYjwC74Dr8tMT1TQcVDvywB85e9
mA3uz8mGKfiMk2TKD6+6on0UwBMB8NbKSB5bcgp+CJ2ceeblmCOgOcOcV5PCGoFj
fgAppr7HjfNPYaIV5l59LfKo2Bj9kXPMqA6/D4gJ3hwoJdY/NOtuNyk8cxWMnWUe
+E2Mm6ZnB3Y=
-----END CERTIFICATE-----

View file

@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDZDCCAhugAwIBAgIUbo9YpgkeniC5jRmOgFYX3mcVJoEwPgYJKoZIhvcNAQEK
MDGgDTALBglghkgBZQMEAgOhGjAYBgkqhkiG9w0BAQgwCwYJYIZIAWUDBAIDogQC
AgC+MBMxETAPBgNVBAMMCE9NSSBSb290MB4XDTIwMDkwNDE4NTMyN1oXDTIxMDkw
NDE4NTMyN1owGDEWMBQGA1UEAwwNREMwMS5vbWkudGVzdDCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBANZMAyRDBn54RfveeVtikepqsyKVKooAc471snl5
mEEeo6ZvlOrK1VGGmo/VlF4R9iW6f5iqxG792KXk+lDtx8sbapZWk/aQa+6I9wml
p17ocW4Otl7XyQ74UTQlxmrped0rgOk+I2Wu3IC7k64gmf/ZbL9mYN/+v8TlYYyO
l8DQbO61XWOJpWt7yf18OxZtPcHH0dkoTEyIxIQcv6FDFNvPjmJzubpDgsfnly7R
C0Rc2yPU702vmAfF0SGQbd6KoXUqlfy26C85vU0Fqom1Qo22ehKrfU50vZrXdaJ2
gX14pm2kuubMjHtX/+bhNyWTxq4anCOl9/aoObZCM1D3+Y8CAwEAAaNJMEcwCQYD
VR0TBAIwADALBgNVHQ8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwGAYDVR0R
BBEwD4INREMwMS5vbWkudGVzdDA+BgkqhkiG9w0BAQowMaANMAsGCWCGSAFlAwQC
A6EaMBgGCSqGSIb3DQEBCDALBglghkgBZQMEAgOiBAICAL4DggEBAHgTDTn8onIi
XFLZ3sWJ5xCBvXypqC37dKALvXxNSo75SQZpOioG4GSdW3zjJWCiudGs7BselkFv
sHK7+5sLKTl1RvxeUoyTxnPZZmVlD3yLq8JBPxu5NScvcRwAcgm3stkq0irRnh7M
4Clw6oSKCKI7Lc3gnbvR3QLSYHeZpUcQgVCad6O/Hi+vxFMJT8PVigG0YUoTW010
pDpi5uh18RxCqRJnnEC7aDrVarxD9aAvqp1wqwWShfP4FZ9m57DH81RTGD2ZzGgP
MsZU5JHVYKkO7IKKIBKuLu+O+X2aZZ4OMlMNBt2DUIJGzEBYV41+3TST9bBPD8xt
AAIFCBcgUYY=
-----END CERTIFICATE-----

View file

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDGzCCAgOgAwIBAgIQJzshhViMG5hLHIJHxa+TcTANBgkqhkiG9w0
BAQQFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD
MxNloXDTE4MDUzMDA4MjMxNlowFTETMBEGA1UEAwwKU0VSVkVSMjAxN
jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAN9N5GAzI7uq
AVlI6vUqhY5+EZWCWWGRwR3FT2DEXE5++AiJxXO0i0ZfAkLu7UggtBe
QwVNkaPD27EYzVUhy1iDo37BrFcLNpfjsjj8wVjaSmQmqvLvrvEh/BT
C5SBgDrk2+hiMh9PrpJoB3QAMDinz5aW0rEXMKitPBBiADrczyYrliF
AlEU6pTlKEKDUAeP7dKOBlDbCYvBxKnR3ddVH74I5T2SmNBq5gzkbKP
nlCXdHLZSh74USu93rKDZQF8YzdTO5dcBreJDJsntyj1o49w9WCt6M7
+pg6vKvE+tRbpCm7kXq5B9PDi42Nb6//MzNaMYf9V7v5MHapvVSv3+y
sCAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
A1UdDgQWBBTh4L2Clr9ber6yfY3JFS3wiECL4DANBgkqhkiG9w0BAQQ
FAAOCAQEA0JK/SL7SP9/nvqWp52vnsxVefTFehThle5DLzagmms/9gu
oSE2I9XkQIttFMprPosaIZWt7WP42uGcZmoZOzU8kFFYJMfg9Ovyca+
gnG28jDUMF1E74KrC7uynJiQJ4vPy8ne7F3XJ592LsNJmK577l42gAW
u08p3TvEJFNHy2dBk/IwZp0HIPr9+JcPf7v0uL6lK930xHJHP56XLzN
YG8vCMpJFR7wVZp3rXkJQUy3GxyHPJPjS8S43I9j+PoyioWIMEotq2+
q0IpXU/KeNFkdGV6VPCmzhykijExOMwO6doUzIUM8orv9jYLHXYC+i6
IFKSb6runxF1MAik+GCSA==
-----END CERTIFICATE-----

View file

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDGzCCAgOgAwIBAgIQUDHcKGevZohJV+TkIIYC1DANBgkqhkiG9w0
BAQ0FADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD
MxN1oXDTE4MDUzMDA4MjMxN1owFTETMBEGA1UEAwwKU0VSVkVSMjAxN
jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKr9bo/XXvHt
D6Qnhb1wyLg9lDQxxe/enH49LQihtVTZMwGf2010h81QrRUe/bkHTvw
K22s2lqj3fUpGxtEbYFWLAHxv6IFnIKd+Zi1zaCPGfas9ekqCSj3vZQ
j7lCJVGUGuuqnSDvsed6g2Pz/g6mJUa+TzjxN+8wU5oj5YVUK+aing1
zPSA2MDCfx3+YzjxVwNoGixOz6Yx9ijT4pUsAYQAf1o9R+6W1/IpGgu
oax714QILT9heqIowwlHzlUZc1UAYs0/JA4CbDZaw9hlJyzMqe/aE46
efqPDOpO3vCpOSRcSyzh02WijPvEEaPejQRWg8RX93othZ615MT7dqp
ECAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
A1UdDgQWBBTgod3R6vejt6kOASAApA19xIG6kTANBgkqhkiG9w0BAQ0
FAAOCAQEAVfz0okK2bh3OQE8cWNbJ5PjJRSAJEqVUvYaTlS0Nqkyuaj
gicP3hb/pF8FvaVaB6r7LqgBxyW5NNL1xwdNLt60M2zaULL6Fhm1vzM
sSMc2ynkyN4++ODwii674YcQAnkUh+ZGIx+CTdZBWJfVM9dZb7QjgBT
nVukeFwN2EOOBSpiQSBpcoeJEEAq9csDVRhEfcB8Wtz7TTItgOVsilY
dQY56ON5XszjCki6UA3GwdQbBEHjWF2WERqXWrojrSSNOYDvxM5mrEx
sG1npzUTsaIr9w8ty1beh/2aToCMREvpiPFOXnVV/ovHMU1lFQTNeQ0
OI7elR0nJ0peai30eMpQQ=='
-----END CERTIFICATE-----

View file

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDGzCCAgOgAwIBAgIQJg/Mf5sR55xApJRK+kabbTANBgkqhkiG9w0
BAQUFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD
MxNloXDTE4MDUzMDA4MjMxNlowFTETMBEGA1UEAwwKU0VSVkVSMjAxN
jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALPKwYikjbzL
Lo6JtS6cyytdMMjSrggDoTnRUKauC5/izoYJd+2YVR5YqnluBJZpoFp
hkCgFFohUOU7qUsI1SkuGnjI8RmWTrrDsSy62BrfX+AXkoPlXo6IpHz
HaEPxjHJdUACpn8QVWTPmdAhwTwQkeUutrm3EOVnKPX4bafNYeAyj7/
AGEplgibuXT4/ehbzGKOkRN3ds/pZuf0xc4Q2+gtXn20tQIUt7t6iwh
nEWjIgopFL/hX/r5q5MpF6stc1XgIwJjEzqMp76w/HUQVqaYneU4qSG
f90ANK/TQ3aDbUNtMC/ULtIfHqHIW4POuBYXaWBsqalJL2VL3YYkKTU
sCAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
A1UdDgQWBBS1jgojcjPu9vqeP1uSKuiIonGwAjANBgkqhkiG9w0BAQU
FAAOCAQEAKjHL6k5Dv/Zb7dvbYEZyx0wVhjHkCTpT3xstI3+TjfAFsu
3zMmyFqFqzmr4pWZ/rHc3ObD4pEa24kP9hfB8nmr8oHMLebGmvkzh5h
0GYc4dIH7Ky1yfQN51hi7/X5iN7jnnBoCJTTlgeBVYDOEBXhfXi3cLT
u3d7nz2heyNq07gFP8iN7MfqdPZndVDYY82imLgsgar9w5d+fvnYM+k
XWItNNCUH18M26Obp4Es/Qogo/E70uqkMHost2D+tww/7woXi36X3w/
D2yBDyrJMJKZLmDgfpNIeCimncTOzi2IhzqJiOY/4XPsVN/Xqv0/dzG
TDdI11kPLq4EiwxvPanCg==
-----END CERTIFICATE-----

View file

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDGzCCAgOgAwIBAgIQWkeAtqoFg6pNWF7xC4YXhTANBgkqhkiG9w0
BAQsFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUyNzA5MD
I0NFoXDTE4MDUyNzA5MjI0NFowFTETMBEGA1UEAwwKU0VSVkVSMjAxN
jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALIPKM5uykFy
NmVoLyvPSXGk15ZDqjYi3AbUxVFwCkVImqhefLATit3PkTUYFtAT+TC
AwK2E4lOu1XHM+Tmp2KIOnq2oUR8qMEvfxYThEf1MHxkctFljFssZ9N
vASDD4lzw8r0Bhl+E5PhR22Eu1Wago5bvIldojkwG+WBxPQv3ZR546L
MUZNaBXC0RhuGj5w83lbVz75qM98wvv1ekfZYAP7lrVyHxqCTPDomEU
I45tQQZHCZl5nRx1fPCyyYfcfqvFlLWD4Q3PZAbnw6mi0MiWJbGYKME
1XGicjqyn/zM9XKA1t/JzChS2bxf6rsyA9I7ibdRHUxsm1JgKry2jfW
0CAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
A1UdDgQWBBQabLGWg1sn7AXPwYPyfE0ER921ZDANBgkqhkiG9w0BAQs
FAAOCAQEAnRohyl6ZmOsTWCtxOJx5A8yr//NweXKwWWmFQXRmCb4bMC
xhD4zqLDf5P6RotGV0I/SHvqz+pAtJuwmr+iyAF6WTzo3164LCfnQEu
psfrrfMkf3txgDwQkA0oPAw3HEwOnR+tzprw3Yg9x6UoZEhi4XqP9AX
R49jU92KrNXJcPlz5MbkzNo5t9nr2f8q39b5HBjaiBJxzdM1hxqsbfD
KirTYbkUgPlVOo/NDmopPPb8IX8ubj/XETZG2jixD0zahgcZ1vdr/iZ
+50WSXKN2TAKBO2fwoK+2/zIWrGRxJTARfQdF+fGKuj+AERIFNh88HW
xSDYjHQAaFMcfdUpa9GGQ==
-----END CERTIFICATE-----

View file

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDGzCCAgOgAwIBAgIQEmj1prSSQYRL2zYBEjsm5jANBgkqhkiG9w0
BAQwFADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD
MxN1oXDTE4MDUzMDA4MjMxN1owFTETMBEGA1UEAwwKU0VSVkVSMjAxN
jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKsK5NvHi4xO
081fRLMmPqKsKaHvXgPRykLA0SmKxpGJHfTAZzxojHVeVwOm87IvQj2
JUh/yrRwSi5Oqrvqx29l2IC/qQt2xkAQsO51/EWkMQ5OSJsl1MN3NXW
eRTKVoUuJzBs8XLmeraxQcBPyyLhq+WpMl/Q4ZDn1FrUEZfxV0POXgU
dI3ApuQNRtJOb6iteBIoQyMlnof0RswBUnkiWCA/+/nzR0j33j47IfL
nkmU4RtqkBlO13f6+e1GZ4lEcQVI2yZq4Zgu5VVGAFU2lQZ3aEVMTu9
8HEqD6heyNp2on5G/K/DCrGWYCBiASjnX3wiSz0BYv8f3HhCgIyVKhJ
8CAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
A1UdDgQWBBQS/SI61S2UE8xwSgHxbkCTpZXo4TANBgkqhkiG9w0BAQw
FAAOCAQEAMVV/WMXd9w4jtDfSrIsKaWKGtHtiMPpAJibXmSakBRwLOn
5ZGXL2bWI/Ac2J2Y7bSzs1im2ifwmEqwzzqnpVKShIkZmtij0LS0SEr
6Fw5IrK8tD6SH+lMMXUTvp4/lLQlgRCwOWxry/YhQSnuprx8IfSPvil
kwZ0Ysim4Aa+X5ojlhHpWB53edX+lFrmR1YWValBnQ5DvnDyFyLR6II
Ialp4vmkzI9e3/eOgSArksizAhpXpC9dxQBiHXdhredN0X+1BVzbgzV
hQBEwgnAIPa+B68oDILaV0V8hvxrP6jFM4IrKoGS1cq0B+Ns0zkG7ZA
2Q0W+3nVwSxIr6bd6hw7g==
-----END CERTIFICATE-----

View file

@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDGzCCAgOgAwIBAgIQUDHcKGevZohJV+TkIIYC1DANBgkqhkiG9w0
BAQ0FADAVMRMwEQYDVQQDDApTRVJWRVIyMDE2MB4XDTE3MDUzMDA4MD
MxN1oXDTE4MDUzMDA4MjMxN1owFTETMBEGA1UEAwwKU0VSVkVSMjAxN
jCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKr9bo/XXvHt
D6Qnhb1wyLg9lDQxxe/enH49LQihtVTZMwGf2010h81QrRUe/bkHTvw
K22s2lqj3fUpGxtEbYFWLAHxv6IFnIKd+Zi1zaCPGfas9ekqCSj3vZQ
j7lCJVGUGuuqnSDvsed6g2Pz/g6mJUa+TzjxN+8wU5oj5YVUK+aing1
zPSA2MDCfx3+YzjxVwNoGixOz6Yx9ijT4pUsAYQAf1o9R+6W1/IpGgu
oax714QILT9heqIowwlHzlUZc1UAYs0/JA4CbDZaw9hlJyzMqe/aE46
efqPDOpO3vCpOSRcSyzh02WijPvEEaPejQRWg8RX93othZ615MT7dqp
ECAwEAAaNnMGUwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGA
QUFBwMCBggrBgEFBQcDATAVBgNVHREEDjAMggpTRVJWRVIyMDE2MB0G
A1UdDgQWBBTgod3R6vejt6kOASAApA19xIG6kTANBgkqhkiG9w0BAQ0
FAAOCAQEAVfz0okK2bh3OQE8cWNbJ5PjJRSAJEqVUvYaTlS0Nqkyuaj
gicP3hb/pF8FvaVaB6r7LqgBxyW5NNL1xwdNLt60M2zaULL6Fhm1vzM
sSMc2ynkyN4++ODwii674YcQAnkUh+ZGIx+CTdZBWJfVM9dZb7QjgBT
nVukeFwN2EOOBSpiQSBpcoeJEEAq9csDVRhEfcB8Wtz7TTItgOVsilY
dQY56ON5XszjCki6UA3GwdQbBEHjWF2WERqXWrojrSSNOYDvxM5mrEx
sG1npzUTsaIr9w8ty1beh/2aToCMREvpiPFOXnVV/ovHMU1lFQTNeQ0
OI7elR0nJ0peai30eMpQQ=='
-----END CERTIFICATE-----

View file

@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-
# (c) 2020 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import base64
import os.path
import pytest
import ansible.module_utils.urls as urls
@pytest.mark.skipif(not urls.HAS_CRYPTOGRAPHY, reason='Requires cryptography to be installed')
@pytest.mark.parametrize('certificate, expected', [
('rsa_md5.pem', b'\x23\x34\xB8\x47\x6C\xBF\x4E\x6D'
b'\xFC\x76\x6A\x5D\x5A\x30\xD6\x64'
b'\x9C\x01\xBA\xE1\x66\x2A\x5C\x3A'
b'\x13\x02\xA9\x68\xD7\xC6\xB0\xF6'),
('rsa_sha1.pem', b'\x14\xCF\xE8\xE4\xB3\x32\xB2\x0A'
b'\x34\x3F\xC8\x40\xB1\x8F\x9F\x6F'
b'\x78\x92\x6A\xFE\x7E\xC3\xE7\xB8'
b'\xE2\x89\x69\x61\x9B\x1E\x8F\x3E'),
('rsa_sha256.pem', b'\x99\x6F\x3E\xEA\x81\x2C\x18\x70'
b'\xE3\x05\x49\xFF\x9B\x86\xCD\x87'
b'\xA8\x90\xB6\xD8\xDF\xDF\x4A\x81'
b'\xBE\xF9\x67\x59\x70\xDA\xDB\x26'),
('rsa_sha384.pem', b'\x34\xF3\x03\xC9\x95\x28\x6F\x4B'
b'\x21\x4A\x9B\xA6\x43\x5B\x69\xB5'
b'\x1E\xCF\x37\x58\xEA\xBC\x2A\x14'
b'\xD7\xA4\x3F\xD2\x37\xDC\x2B\x1A'
b'\x1A\xD9\x11\x1C\x5C\x96\x5E\x10'
b'\x75\x07\xCB\x41\x98\xC0\x9F\xEC'),
('rsa_sha512.pem', b'\x55\x6E\x1C\x17\x84\xE3\xB9\x57'
b'\x37\x0B\x7F\x54\x4F\x62\xC5\x33'
b'\xCB\x2C\xA5\xC1\xDA\xE0\x70\x6F'
b'\xAE\xF0\x05\x44\xE1\xAD\x2B\x76'
b'\xFF\x25\xCF\xBE\x69\xB1\xC4\xE6'
b'\x30\xC3\xBB\x02\x07\xDF\x11\x31'
b'\x4C\x67\x38\xBC\xAE\xD7\xE0\x71'
b'\xD7\xBF\xBF\x2C\x9D\xFA\xB8\x5D'),
('rsa-pss_sha256.pem', b'\xF2\x31\xE6\xFF\x3F\x9E\x16\x1B'
b'\xC2\xDC\xBB\x89\x8D\x84\x47\x4E'
b'\x58\x9C\xD7\xC2\x7A\xDB\xEF\x8B'
b'\xD9\xC0\xC0\x68\xAF\x9C\x36\x6D'),
('rsa-pss_sha512.pem', b'\x85\x85\x19\xB9\xE1\x0F\x23\xE2'
b'\x1D\x2C\xE9\xD5\x47\x2A\xAB\xCE'
b'\x42\x0F\xD1\x00\x75\x9C\x53\xA1'
b'\x7B\xB9\x79\x86\xB2\x59\x61\x27'),
('ecdsa_sha256.pem', b'\xFE\xCF\x1B\x25\x85\x44\x99\x90'
b'\xD9\xE3\xB2\xC9\x2D\x3F\x59\x7E'
b'\xC8\x35\x4E\x12\x4E\xDA\x75\x1D'
b'\x94\x83\x7C\x2C\x89\xA2\xC1\x55'),
('ecdsa_sha512.pem', b'\xE5\xCB\x68\xB2\xF8\x43\xD6\x3B'
b'\xF4\x0B\xCB\x20\x07\x60\x8F\x81'
b'\x97\x61\x83\x92\x78\x3F\x23\x30'
b'\xE5\xEF\x19\xA5\xBD\x8F\x0B\x2F'
b'\xAA\xC8\x61\x85\x5F\xBB\x63\xA2'
b'\x21\xCC\x46\xFC\x1E\x22\x6A\x07'
b'\x24\x11\xAF\x17\x5D\xDE\x47\x92'
b'\x81\xE0\x06\x87\x8B\x34\x80\x59'),
])
def test_cbt_with_cert(certificate, expected):
with open(os.path.join(os.path.dirname(__file__), 'fixtures', 'cbt', certificate)) as fd:
cert_der = base64.b64decode("".join([l.strip() for l in fd.readlines()[1:-1]]))
actual = urls.get_channel_binding_cert_hash(cert_der)
assert actual == expected
def test_cbt_no_cryptography(monkeypatch):
monkeypatch.setattr(urls, 'HAS_CRYPTOGRAPHY', False)
assert urls.get_channel_binding_cert_hash(None) is None