[AMQP 1.0] Fix SSL client authentication

The driver incorrectly set up SSL client authentication.  This patch
fixes the configuration. Unit tests added.

Change-Id: I64b1736f5a708c70013d9fedba73da4fa8d9ccfb
Closes-Bug: #1606985
This commit is contained in:
Kenneth Giusti 2017-01-05 15:11:35 -05:00
parent e90792de85
commit 53ead5c11d
3 changed files with 172 additions and 10 deletions

View File

@ -978,14 +978,9 @@ class Controller(pyngus.ConnectionEventHandler):
if self.ssl_ca_file:
conn_props["x-ssl-ca-file"] = self.ssl_ca_file
if self.ssl_cert_file:
# assume this connection is for a server. If client authentication
# support is developed, we'll need an explicit flag (server or
# client)
conn_props["x-ssl-server"] = True
conn_props["x-ssl-identity"] = (self.ssl_cert_file,
self.ssl_key_file,
self.ssl_key_password)
conn_props["x-ssl-allow-cleartext"] = self.ssl_allow_insecure
# SASL configuration:
if self.sasl_mechanisms:
conn_props["x-sasl-mechs"] = self.sasl_mechanisms

View File

@ -34,17 +34,20 @@ amqp1_opts = [
cfg.StrOpt('ssl_ca_file',
default='',
deprecated_group='amqp1',
help="CA certificate PEM file to verify server certificate"),
help="CA certificate PEM file used to verify the server's"
' certificate'),
cfg.StrOpt('ssl_cert_file',
default='',
deprecated_group='amqp1',
help='Identifying certificate PEM file to present to clients'),
help='Self-identifying certificate PEM file'
' for client authentication'),
cfg.StrOpt('ssl_key_file',
default='',
deprecated_group='amqp1',
help='Private key PEM file used to sign cert_file certificate'),
help='Private key PEM file used to sign ssl_cert_file'
' certificate (optional)'),
cfg.StrOpt('ssl_key_password',
deprecated_group='amqp1',
@ -54,6 +57,9 @@ amqp1_opts = [
cfg.BoolOpt('allow_insecure_clients',
default=False,
deprecated_group='amqp1',
# marked as deprecated in Ocata
deprecated_for_removal=True,
deprecated_reason="Not applicable - not a SSL server",
help='Accept clients using either SSL or plain TCP'),
cfg.StrOpt('sasl_mechanisms',

View File

@ -15,10 +15,12 @@
import logging
import os
import select
import shlex
import shutil
import socket
import subprocess
import sys
import tempfile
import threading
import time
import uuid
@ -49,6 +51,8 @@ if pyngus:
_proton = importutils.try_import("proton")
CYRUS_ENABLED = (pyngus and pyngus.VERSION >= (2, 0, 0) and _proton
and getattr(_proton.SASL, "extended", lambda: False)())
# same with SSL
SSL_ENABLED = (_proton and getattr(_proton.SSL, "present", lambda: False)())
LOG = logging.getLogger(__name__)
@ -1449,6 +1453,148 @@ class TestMessageRetransmit(_AmqpBrokerTestCase):
lambda l, h: l.message_rejected(h, {}))
@testtools.skipUnless(SSL_ENABLED, "OpenSSL not supported")
class TestSSL(test_utils.BaseTestCase):
"""Test the driver's OpenSSL integration"""
def setUp(self):
super(TestSSL, self).setUp()
# Create the CA, server, and client SSL certificates:
self._tmpdir = tempfile.mkdtemp(prefix='amqp1')
files = ['ca_key', 'ca_cert', 's_key', 's_req', 's_cert', 'c_key',
'c_req', 'c_cert', 'bad_cert', 'bad_req', 'bad_key']
conf = dict(zip(files, [os.path.join(self._tmpdir, "%s.pem" % f)
for f in files]))
conf['pw'] = 'password'
conf['s_name'] = '127.0.0.1'
conf['c_name'] = 'client.com'
self._ssl_config = conf
ssl_setup = [
# create self-signed CA key and certificate:
Template('openssl genrsa -out ${ca_key} 2048').substitute(conf),
Template('openssl req -x509 -key ${ca_key} -subj'
' "/CN=Trusted.CA.com" -out'
' ${ca_cert}').substitute(conf),
# create Server key and certificate:
Template('openssl genrsa -out ${s_key} 2048').substitute(conf),
Template('openssl req -new -key ${s_key} -subj /CN=${s_name}'
' -passin pass:${pw} -out ${s_req}').substitute(conf),
Template('openssl x509 -req -in ${s_req} -CA ${ca_cert}'
' -CAkey ${ca_key} -CAcreateserial -out'
' ${s_cert}').substitute(conf),
# create a "bad" Server cert for testing CN validation:
Template('openssl genrsa -out ${bad_key} 2048').substitute(conf),
Template('openssl req -new -key ${bad_key} -subj /CN=Invalid'
' -passin pass:${pw} -out ${bad_req}').substitute(conf),
Template('openssl x509 -req -in ${bad_req} -CA ${ca_cert}'
' -CAkey ${ca_key} -CAcreateserial -out'
' ${bad_cert}').substitute(conf),
# create Client key and certificate for client authentication:
Template('openssl genrsa -out ${c_key} 2048').substitute(conf),
Template('openssl req -new -key ${c_key} -subj /CN=${c_name}'
' -passin pass:${pw} -out'
' ${c_req}').substitute(conf),
Template('openssl x509 -req -in ${c_req} -CA ${ca_cert}'
' -CAkey ${ca_key} -CAcreateserial -out'
' ${c_cert}').substitute(conf)
]
for cmd in ssl_setup:
try:
subprocess.check_call(args=shlex.split(cmd))
except Exception:
shutil.rmtree(self._tmpdir, ignore_errors=True)
self._tmpdir = None
self.skipTest("OpenSSL tools not installed - skipping")
def test_server_ok(self):
# test client authenticates server
self._broker = FakeBroker(self.conf.oslo_messaging_amqp,
sock_addr=self._ssl_config['s_name'],
ssl_config=self._ssl_config)
url = oslo_messaging.TransportURL.parse(self.conf, "amqp://%s:%d" %
(self._broker.host,
self._broker.port))
self._broker.start()
self.config(ssl_ca_file=self._ssl_config['ca_cert'],
group='oslo_messaging_amqp')
driver = amqp_driver.ProtonDriver(self.conf, url)
target = oslo_messaging.Target(topic="test-topic")
listener = _ListenerThread(
driver.listen(target, None, None)._poll_style_listener, 1)
driver.send(target,
{"context": "whatever"},
{"method": "echo", "a": "b"},
wait_for_reply=True,
timeout=30)
listener.join(timeout=30)
self.assertFalse(listener.isAlive())
driver.cleanup()
def test_bad_server_fail(self):
# test client does not connect to invalid server
self._ssl_config['s_cert'] = self._ssl_config['bad_cert']
self._ssl_config['s_key'] = self._ssl_config['bad_key']
self._broker = FakeBroker(self.conf.oslo_messaging_amqp,
sock_addr=self._ssl_config['s_name'],
ssl_config=self._ssl_config)
url = oslo_messaging.TransportURL.parse(self.conf, "amqp://%s:%d" %
(self._broker.host,
self._broker.port))
self._broker.start()
self.config(ssl_ca_file=self._ssl_config['ca_cert'],
group='oslo_messaging_amqp')
driver = amqp_driver.ProtonDriver(self.conf, url)
target = oslo_messaging.Target(topic="test-topic")
self.assertRaises(oslo_messaging.MessageDeliveryFailure,
driver.send, target,
{"context": "whatever"},
{"method": "echo", "a": "b"},
wait_for_reply=False,
retry=1)
driver.cleanup()
def test_client_auth_ok(self):
# test server authenticates client
self._ssl_config['authenticate_client'] = True
self._broker = FakeBroker(self.conf.oslo_messaging_amqp,
sock_addr=self._ssl_config['s_name'],
ssl_config=self._ssl_config)
url = oslo_messaging.TransportURL.parse(self.conf, "amqp://%s:%d" %
(self._broker.host,
self._broker.port))
self._broker.start()
self.config(ssl_ca_file=self._ssl_config['ca_cert'],
ssl_cert_file=self._ssl_config['c_cert'],
ssl_key_file=self._ssl_config['c_key'],
ssl_key_password=self._ssl_config['pw'],
group='oslo_messaging_amqp')
driver = amqp_driver.ProtonDriver(self.conf, url)
target = oslo_messaging.Target(topic="test-topic")
listener = _ListenerThread(
driver.listen(target, None, None)._poll_style_listener, 1)
driver.send(target,
{"context": "whatever"},
{"method": "echo", "a": "b"},
wait_for_reply=True,
timeout=30)
listener.join(timeout=30)
self.assertFalse(listener.isAlive())
driver.cleanup()
def tearDown(self):
if self._broker:
self._broker.stop()
self._broker = None
if self._tmpdir:
shutil.rmtree(self._tmpdir, ignore_errors=True)
super(TestSSL, self).tearDown()
class FakeBroker(threading.Thread):
"""A test AMQP message 'broker'."""
@ -1466,6 +1612,7 @@ class FakeBroker(threading.Thread):
self.sasl_mechanisms = sasl_mechanisms
self.user_credentials = user_credentials
properties = {'x-server': True}
# setup SASL:
if self.sasl_mechanisms:
properties['x-sasl-mechs'] = self.sasl_mechanisms
if "ANONYMOUS" not in self.sasl_mechanisms:
@ -1474,6 +1621,19 @@ class FakeBroker(threading.Thread):
properties['x-sasl-config-dir'] = sasl_config_dir
if sasl_config_name:
properties['x-sasl-config-name'] = sasl_config_name
# setup SSL
if self.server._ssl_config:
ssl = self.server._ssl_config
properties['x-ssl-server'] = True
properties['x-ssl-identity'] = (ssl['s_cert'],
ssl['s_key'],
ssl['pw'])
# check for client authentication
if ssl.get('authenticate_client'):
properties['x-ssl-ca-file'] = ssl['ca_cert']
properties['x-ssl-verify-mode'] = 'verify-peer'
properties['x-ssl-peer-name'] = ssl['c_name']
# misc connection properties
if product:
properties['properties'] = {'product': product}
@ -1681,17 +1841,18 @@ class FakeBroker(threading.Thread):
sasl_mechanisms="ANONYMOUS",
user_credentials=None,
sasl_config_dir=None,
sasl_config_name=None):
sasl_config_name=None,
ssl_config=None):
"""Create a fake broker listening on sock_addr:sock_port."""
if not pyngus:
raise AssertionError("pyngus module not present")
threading.Thread.__init__(self)
self._config = cfg
self._product = product
self._sasl_mechanisms = sasl_mechanisms
self._sasl_config_dir = sasl_config_dir
self._sasl_config_name = sasl_config_name
self._user_credentials = user_credentials
self._ssl_config = ssl_config
self._wakeup_pipe = os.pipe()
self._my_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self._my_socket.bind((sock_addr, sock_port))