diff options
author | Kenneth Anthony Giusti <kgiusti@apache.org> | 2013-03-22 21:45:42 +0000 |
---|---|---|
committer | Kenneth Anthony Giusti <kgiusti@apache.org> | 2013-03-22 21:45:42 +0000 |
commit | 4b532586d1a44628ffa037fa131f4f314592fda5 (patch) | |
tree | 4f63419b700a939aeacf13334ceb204e7c46bb07 | |
parent | f4d589d6ab87cef16be5383796e23039a0c820e4 (diff) | |
download | qpid-python-4b532586d1a44628ffa037fa131f4f314592fda5.tar.gz |
NO-JIRA: add SSL test that verifies hostname in certificate
git-svn-id: https://svn.apache.org/repos/asf/qpid/trunk@1460013 13f79535-47bb-0310-9956-ffa450edef68
-rwxr-xr-x | qpid/cpp/src/tests/ping_broker | 7 | ||||
-rw-r--r-- | qpid/cpp/src/tests/ssl.mk | 2 | ||||
-rwxr-xr-x | qpid/cpp/src/tests/ssl_test | 144 | ||||
-rw-r--r-- | qpid/python/qpid/messaging/endpoints.py | 5 | ||||
-rw-r--r-- | qpid/python/qpid/messaging/transports.py | 71 |
5 files changed, 214 insertions, 15 deletions
diff --git a/qpid/cpp/src/tests/ping_broker b/qpid/cpp/src/tests/ping_broker index 6c391027a3..be99a6ef46 100755 --- a/qpid/cpp/src/tests/ping_broker +++ b/qpid/cpp/src/tests/ping_broker @@ -60,6 +60,9 @@ def OptionsAndArguments(argv): help="SASL mechanism for authentication (e.g. EXTERNAL, ANONYMOUS, PLAIN, CRAM-MD, DIGEST-MD5, GSSAPI). SASL automatically picks the most secure available mechanism - use this option to override.") parser.add_option("--ssl-certificate", action="store", type="string", metavar="<cert>", help="Client SSL certificate (PEM Format)") parser.add_option("--ssl-key", action="store", type="string", metavar="<key>", help="Client SSL private key (PEM Format)") + parser.add_option("--ssl-trustfile", action="store", type="string", metavar="<CA>", help="List of trusted CAs (PEM Format)") + parser.add_option("--ssl-skip-hostname-check", action="store_true", + help="Do not validate hostname in peer certificate") parser.add_option("--ha-admin", action="store_true", help="Allow connection to a HA backup broker.") opts, args = parser.parse_args(args=argv) @@ -73,6 +76,10 @@ def OptionsAndArguments(argv): conn_options['ssl_certfile'] = opts.ssl_certificate if opts.ssl_key: conn_options['ssl_key'] = opts.ssl_key + if opts.ssl_trustfile: + conn_options['ssl_trustfile'] = opts.ssl_trustfile + if opts.ssl_skip_hostname_check: + conn_options['ssl_skip_hostname_check'] = True if opts.ha_admin: conn_options['client_properties'] = {'qpid.ha-admin' : 1} return args diff --git a/qpid/cpp/src/tests/ssl.mk b/qpid/cpp/src/tests/ssl.mk index 435db0c55b..1544dc5e71 100644 --- a/qpid/cpp/src/tests/ssl.mk +++ b/qpid/cpp/src/tests/ssl.mk @@ -19,4 +19,4 @@ TESTS+=ssl_test EXTRA_DIST+=ssl_test -CLEAN_LOCAL += test_cert_db cert.password +CLEAN_LOCAL += test_cert_dir cert.password diff --git a/qpid/cpp/src/tests/ssl_test b/qpid/cpp/src/tests/ssl_test index 89aaf44af0..9ce2880caa 100755 --- a/qpid/cpp/src/tests/ssl_test +++ b/qpid/cpp/src/tests/ssl_test @@ -22,33 +22,82 @@ # Run a simple test over SSL source ./test_env.sh +#set -x + CONFIG=$(dirname $0)/config.null -CERT_DIR=`pwd`/test_cert_db +TEST_CERT_DIR=`pwd`/test_cert_dir +SERVER_CERT_DIR=${TEST_CERT_DIR}/test_cert_db +CA_CERT_DIR=${TEST_CERT_DIR}/ca_cert_db +OTHER_CA_CERT_DIR=${TEST_CERT_DIR}/x_ca_cert_db CERT_PW_FILE=`pwd`/cert.password TEST_HOSTNAME=127.0.0.1 TEST_CLIENT_CERT=rumplestiltskin +CA_PEM_FILE=${TEST_CERT_DIR}/ca_cert.pem +OTHER_CA_PEM_FILE=${TEST_CERT_DIR}/other_ca_cert.pem +PY_PING_BROKER=$top_srcdir/src/tests/ping_broker COUNT=10 trap cleanup EXIT error() { echo $*; exit 1; } -create_certs() { - #create certificate and key databases with single, simple, self-signed certificate in it - mkdir ${CERT_DIR} - certutil -N -d ${CERT_DIR} -f ${CERT_PW_FILE} - certutil -S -d ${CERT_DIR} -n ${TEST_HOSTNAME} -s "CN=${TEST_HOSTNAME}" -t "CT,," -x -f ${CERT_PW_FILE} -z /usr/bin/certutil - certutil -S -d ${CERT_DIR} -n ${TEST_CLIENT_CERT} -s "CN=${TEST_CLIENT_CERT}" -t "CT,," -x -f ${CERT_PW_FILE} -z /usr/bin/certutil +create_ca_certs() { + + # Set Up the CA DB and self-signed Certificate + # + mkdir -p ${CA_CERT_DIR} + certutil -N -d ${CA_CERT_DIR} -f ${CERT_PW_FILE} + certutil -S -d ${CA_CERT_DIR} -n "Test-CA" -s "CN=Test-CA,O=MyCo,ST=Massachusetts,C=US" -t "CT,," -x -f ${CERT_PW_FILE} -z /bin/sh >/dev/null 2>&1 + certutil -L -d ${CA_CERT_DIR} -n "Test-CA" -a -o ${CA_CERT_DIR}/rootca.crt -f ${CERT_PW_FILE} + #certutil -L -d ${CA_CERT_DIR} -f ${CERT_PW_FILE} + + # Set Up another CA DB for testing failure to validate scenario + # + mkdir -p ${OTHER_CA_CERT_DIR} + certutil -N -d ${OTHER_CA_CERT_DIR} -f ${CERT_PW_FILE} + certutil -S -d ${OTHER_CA_CERT_DIR} -n "Other-Test-CA" -s "CN=Another Test CA,O=MyCo,ST=Massachusetts,C=US" -t "CT,," -x -f ${CERT_PW_FILE} -z /bin/sh >/dev/null 2>&1 + certutil -L -d ${OTHER_CA_CERT_DIR} -n "Other-Test-CA" -a -o ${OTHER_CA_CERT_DIR}/rootca.crt -f ${CERT_PW_FILE} + #certutil -L -d ${OTHER_CA_CERT_DIR} -f ${CERT_PW_FILE} +} + +# create server certificate signed by Test-CA +# $1 = string used as Subject in certificate +# $2 = string used as SubjectAlternateName (SAN) in certificate +create_server_cert() { + mkdir -p ${SERVER_CERT_DIR} + rm -rf ${SERVER_CERT_DIR}/* + + local CERT_SUBJECT=${1:-"CN=${TEST_HOSTNAME},O=MyCo,ST=Massachusetts,C=US"} + local CERT_SAN=${2:-"*.server.com"} + + # create database + certutil -N -d ${SERVER_CERT_DIR} -f ${CERT_PW_FILE} + # create certificate request + certutil -R -d ${SERVER_CERT_DIR} -s "${CERT_SUBJECT}" -8 "${CERT_SAN}" -o server.req -f ${CERT_PW_FILE} -z /bin/sh > /dev/null 2>&1 + # have CA sign it + certutil -C -d ${CA_CERT_DIR} -c "Test-CA" -i server.req -o server.crt -f ${CERT_PW_FILE} -m ${RANDOM} + # add it to the database + certutil -A -d ${SERVER_CERT_DIR} -n ${TEST_HOSTNAME} -i server.crt -t "Pu,," + rm server.req server.crt + + # now create a certificate for the client + certutil -R -d ${SERVER_CERT_DIR} -s "CN=${TEST_CLIENT_CERT}" -8 "*.client.com" -o client.req -f ${CERT_PW_FILE} -z /bin/sh > /dev/null 2>&1 + certutil -C -d ${CA_CERT_DIR} -c "Test-CA" -i client.req -o client.crt -f ${CERT_PW_FILE} -m ${RANDOM} + certutil -A -d ${SERVER_CERT_DIR} -n ${TEST_CLIENT_CERT} -i client.crt -t "Pu,," + ### + #certutil -N -d ${SERVER_CERT_DIR} -f ${CERT_PW_FILE} + #certutil -S -d ${SERVER_CERT_DIR} -n ${TEST_HOSTNAME} -s "CN=${TEST_HOSTNAME}" -t "CT,," -x -f ${CERT_PW_FILE} -z /usr/bin/certutil + #certutil -S -d ${SERVER_CERT_DIR} -n ${TEST_CLIENT_CERT} -s "CN=${TEST_CLIENT_CERT}" -t "CT,," -x -f ${CERT_PW_FILE} -z /usr/bin/certutil } delete_certs() { - if [[ -e ${CERT_DIR} ]] ; then - rm -rf ${CERT_DIR} + if [[ -e ${TEST_CERT_DIR} ]] ; then + rm -rf ${TEST_CERT_DIR} fi } # Don't need --no-module-dir or --no-data-dir as they are set as env vars in test_env.sh -COMMON_OPTS="--daemon --config $CONFIG --load-module $SSL_LIB --ssl-cert-db $CERT_DIR --ssl-cert-password-file $CERT_PW_FILE --ssl-cert-name $TEST_HOSTNAME" +COMMON_OPTS="--daemon --config $CONFIG --load-module $SSL_LIB --ssl-cert-db $SERVER_CERT_DIR --ssl-cert-password-file $CERT_PW_FILE --ssl-cert-name $TEST_HOSTNAME" # Start new brokers: # $1 must be integer @@ -89,6 +138,7 @@ pick_port() { cleanup() { stop_brokers delete_certs + rm -f ${CERT_PW_FILE} } start_ssl_broker() { @@ -123,14 +173,15 @@ if [[ !(-e ${CERT_PW_FILE}) ]] ; then echo password > ${CERT_PW_FILE} fi delete_certs -create_certs || error "Could not create test certificate" +create_ca_certs || error "Could not create test certificate" +create_server_cert || error "Could not create server test certificate" start_ssl_broker PORT=${PORTS[0]} echo "Running SSL test on port $PORT" export QPID_NO_MODULE_DIR=1 export QPID_LOAD_MODULE=$SSLCONNECTOR_LIB -export QPID_SSL_CERT_DB=${CERT_DIR} +export QPID_SSL_CERT_DB=${SERVER_CERT_DIR} export QPID_SSL_CERT_PASSWORD_FILE=${CERT_PW_FILE} ## Test connection via connection settings @@ -193,3 +244,72 @@ echo "Running SSL/TCP mux test on random port $PORT" ./qpid-perftest --count ${COUNT} --port ${PORT} -P tcp -b $TEST_HOSTNAME --summary || error "TCP connection failed!" stop_brokers + +### Additional tests that require 'openssl' and 'pk12util' to be installed (optional) + +PK12UTIL=$(type -p pk12util) +if [[ !(-x $PK12UTIL) ]] ; then + echo >&2 "'pk12util' command not available, skipping remaining tests" + exit 0 +fi + +OPENSSL=$(type -p openssl) +if [[ !(-x $OPENSSL) ]] ; then + echo >&2 "'openssl' command not available, skipping remaining tests" + exit 0 +fi + +## verify python version > 2.5 (only 2.6+ does certificate checking) +py_major=$(python -c "import sys; print sys.version_info[0]") +py_minor=$(python -c "import sys; print sys.version_info[1]") +if (( py_major < 2 || ( py_major == 2 && py_minor < 6 ) )); then + echo >&2 "Detected python version < 2.6 - skipping certificate verification tests" + exit 0 +fi + +echo "Testing Certificate validation and Authentication with the Python Client..." + +# extract the CA's certificate as a PEM file + +$PK12UTIL -o ${TEST_CERT_DIR}/CA_pk12.out -d ${CA_CERT_DIR} -n "Test-CA" -w ${CERT_PW_FILE} -k ${CERT_PW_FILE} > /dev/null +$OPENSSL pkcs12 -in ${TEST_CERT_DIR}/CA_pk12.out -out ${CA_PEM_FILE} -nokeys -passin file:${CERT_PW_FILE} >/dev/null +$PK12UTIL -o ${TEST_CERT_DIR}/other_CA_pk12.out -d ${OTHER_CA_CERT_DIR} -n "Other-Test-CA" -w ${CERT_PW_FILE} -k ${CERT_PW_FILE} > /dev/null +$OPENSSL pkcs12 -in ${TEST_CERT_DIR}/other_CA_pk12.out -out ${OTHER_CA_PEM_FILE} -nokeys -passin file:${CERT_PW_FILE} >/dev/null + +start_ssl_broker +PORT=${PORTS[0]} +URL=amqps://$TEST_HOSTNAME:$PORT +# verify the python client can authenticate the broker using the CA +if `${PY_PING_BROKER} -b $URL --ssl-trustfile=${CA_PEM_FILE}`; then echo " Passed"; else { echo " Failed"; exit 1; }; fi +# verify the python client fails to authenticate the broker when using the other CA +if `${PY_PING_BROKER} -b $URL --ssl-trustfile=${OTHER_CA_PEM_FILE} > /dev/null 2>&1`; then { echo " Failed"; exit 1; }; else echo " Passed"; fi +stop_brokers + +# create a certificate with TEST_HOSTNAME only in SAN, should verify OK + +create_server_cert "O=MyCo" "*.foo.com,${TEST_HOSTNAME},*xyz.com" || error "Could not create server test certificate" +start_ssl_broker +PORT=${PORTS[0]} +URL=amqps://$TEST_HOSTNAME:$PORT +if `${PY_PING_BROKER} -b $URL --ssl-trustfile=${CA_PEM_FILE}`; then echo " Passed"; else { echo " Failed"; exit 1; }; fi +stop_brokers + +create_server_cert "O=MyCo" "*${TEST_HOSTNAME}" || error "Could not create server test certificate" +start_ssl_broker +PORT=${PORTS[0]} +URL=amqps://$TEST_HOSTNAME:$PORT +if `${PY_PING_BROKER} -b $URL --ssl-trustfile=${CA_PEM_FILE}`; then echo " Passed"; else { echo " Failed"; exit 1; }; fi +stop_brokers + +# create a certificate without matching TEST_HOSTNAME, should fail to verify + +create_server_cert "O=MyCo" "*.${TEST_HOSTNAME}.com" || error "Could not create server test certificate" +start_ssl_broker +PORT=${PORTS[0]} +URL=amqps://$TEST_HOSTNAME:$PORT +if `${PY_PING_BROKER} -b $URL --ssl-trustfile=${CA_PEM_FILE} > /dev/null 2>&1`; then { echo " Failed"; exit 1; }; else echo " Passed"; fi +# but disabling the check for the hostname should pass +if `${PY_PING_BROKER} -b $URL --ssl-trustfile=${CA_PEM_FILE} --ssl-skip-hostname-check`; then echo " Passed"; else { echo " Failed"; exit 1; }; fi +stop_brokers + + diff --git a/qpid/python/qpid/messaging/endpoints.py b/qpid/python/qpid/messaging/endpoints.py index 95ff5516d0..143daf616a 100644 --- a/qpid/python/qpid/messaging/endpoints.py +++ b/qpid/python/qpid/messaging/endpoints.py @@ -122,6 +122,10 @@ class Connection(Endpoint): @param ssl_certfile: file with client's public (eventually priv+pub) key (PEM format) @type ssl_trustfile: str @param ssl_trustfile: file trusted certificates to validate the server + @type ssl_skip_hostname_check: bool + @param ssl_skip_hostname_check: disable verification of hostname in + certificate. Use with caution - disabling hostname checking leaves you + vulnerable to Man-in-the-Middle attacks. @rtype: Connection @return: a disconnected Connection @@ -170,6 +174,7 @@ class Connection(Endpoint): self.ssl_keyfile = options.get("ssl_keyfile", None) self.ssl_certfile = options.get("ssl_certfile", None) self.ssl_trustfile = options.get("ssl_trustfile", None) + self.ssl_skip_hostname_check = options.get("ssl_skip_hostname_check", False) self.client_properties = options.get("client_properties", {}) self.options = options diff --git a/qpid/python/qpid/messaging/transports.py b/qpid/python/qpid/messaging/transports.py index e901e98258..c76db1f395 100644 --- a/qpid/python/qpid/messaging/transports.py +++ b/qpid/python/qpid/messaging/transports.py @@ -53,7 +53,7 @@ TRANSPORTS["tcp"] = tcp try: from ssl import wrap_socket, SSLError, SSL_ERROR_WANT_READ, \ - SSL_ERROR_WANT_WRITE + SSL_ERROR_WANT_WRITE, CERT_REQUIRED, CERT_NONE except ImportError: ## try the older python SSL api: @@ -69,6 +69,15 @@ except ImportError: ssl_certfile = conn.ssl_certfile if ssl_certfile and not ssl_keyfile: ssl_keyfile = ssl_certfile + + # this version of SSL does NOT perform certificate validation. If the + # connection has been configured with CA certs (via ssl_trustfile), then + # the application expects the certificate to be validated against the + # supplied CA certs. Since this version cannot validate, the peer cannot + # be trusted. + if conn.ssl_trustfile: + raise SSLError("This version of Python does not support verification of the peer's certificate.") + self.ssl = ssl(self.socket, keyfile=ssl_keyfile, certfile=ssl_certfile) self.socket.setblocking(1) @@ -95,7 +104,39 @@ else: def __init__(self, conn, host, port): SocketTransport.__init__(self, conn, host, port) - self.tls = wrap_socket(self.socket, keyfile=conn.ssl_keyfile, certfile=conn.ssl_certfile, ca_certs=conn.ssl_trustfile) + if conn.ssl_trustfile: + validate = CERT_REQUIRED + else: + validate = CERT_NONE + + self.tls = wrap_socket(self.socket, keyfile=conn.ssl_keyfile, + certfile=conn.ssl_certfile, + ca_certs=conn.ssl_trustfile, + cert_reqs=validate) + + if validate == CERT_REQUIRED and not conn.ssl_skip_hostname_check: + match_found = False + peer_cert = self.tls.getpeercert() + if peer_cert: + peer_names = [] + if 'subjectAltName' in peer_cert: + for san in peer_cert['subjectAltName']: + if san[0] == 'DNS': + peer_names.append(san[1].lower()) + if 'subject' in peer_cert: + for sub in peer_cert['subject']: + while isinstance(sub, tuple) and isinstance(sub[0],tuple): + sub = sub[0] # why the extra level of indirection??? + if sub[0] == 'commonName': + peer_names.append(sub[1].lower()) + for pattern in peer_names: + if _match_dns_pattern( host.lower(), pattern ): + #print "Match found %s" % pattern + match_found = True + break + if not match_found: + raise SSLError("Connection hostname '%s' does not match names from peer certificate: %s" % (host, peer_names)) + self.socket.setblocking(0) self.state = None @@ -146,5 +187,31 @@ else: # this closes the underlying socket self.tls.close() + def _match_dns_pattern( hostname, pattern ): + """ For checking the hostnames provided by the peer's certificate + """ + if pattern.find("*") == -1: + return hostname == pattern + + # DNS wildcarded pattern - see RFC2818 + h_labels = hostname.split(".") + p_labels = pattern.split(".") + + while h_labels and p_labels: + if p_labels[0].find("*") == -1: + if p_labels[0] != h_labels[0]: + return False + else: + p = p_labels[0].split("*") + if not h_labels[0].startswith(p[0]): + return False + if not h_labels[0].endswith(p[1]): + return False + h_labels.pop(0) + p_labels.pop(0) + + return not h_labels and not p_labels + + TRANSPORTS["ssl"] = tls TRANSPORTS["tcp+tls"] = tls |