Author: kgiusti
Date: Fri Mar 22 21:45:42 2013
New Revision: 1460013

URL: http://svn.apache.org/r1460013
Log:
NO-JIRA: add SSL test that verifies hostname in certificate

Modified:
    qpid/trunk/qpid/cpp/src/tests/ping_broker
    qpid/trunk/qpid/cpp/src/tests/ssl.mk
    qpid/trunk/qpid/cpp/src/tests/ssl_test
    qpid/trunk/qpid/python/qpid/messaging/endpoints.py
    qpid/trunk/qpid/python/qpid/messaging/transports.py

Modified: qpid/trunk/qpid/cpp/src/tests/ping_broker
URL: 
http://svn.apache.org/viewvc/qpid/trunk/qpid/cpp/src/tests/ping_broker?rev=1460013&r1=1460012&r2=1460013&view=diff
==============================================================================
--- qpid/trunk/qpid/cpp/src/tests/ping_broker (original)
+++ qpid/trunk/qpid/cpp/src/tests/ping_broker Fri Mar 22 21:45:42 2013
@@ -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

Modified: qpid/trunk/qpid/cpp/src/tests/ssl.mk
URL: 
http://svn.apache.org/viewvc/qpid/trunk/qpid/cpp/src/tests/ssl.mk?rev=1460013&r1=1460012&r2=1460013&view=diff
==============================================================================
--- qpid/trunk/qpid/cpp/src/tests/ssl.mk (original)
+++ qpid/trunk/qpid/cpp/src/tests/ssl.mk Fri Mar 22 21:45:42 2013
@@ -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

Modified: qpid/trunk/qpid/cpp/src/tests/ssl_test
URL: 
http://svn.apache.org/viewvc/qpid/trunk/qpid/cpp/src/tests/ssl_test?rev=1460013&r1=1460012&r2=1460013&view=diff
==============================================================================
--- qpid/trunk/qpid/cpp/src/tests/ssl_test (original)
+++ qpid/trunk/qpid/cpp/src/tests/ssl_test Fri Mar 22 21:45:42 2013
@@ -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
 ./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
+
+

Modified: qpid/trunk/qpid/python/qpid/messaging/endpoints.py
URL: 
http://svn.apache.org/viewvc/qpid/trunk/qpid/python/qpid/messaging/endpoints.py?rev=1460013&r1=1460012&r2=1460013&view=diff
==============================================================================
--- qpid/trunk/qpid/python/qpid/messaging/endpoints.py (original)
+++ qpid/trunk/qpid/python/qpid/messaging/endpoints.py Fri Mar 22 21:45:42 2013
@@ -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

Modified: qpid/trunk/qpid/python/qpid/messaging/transports.py
URL: 
http://svn.apache.org/viewvc/qpid/trunk/qpid/python/qpid/messaging/transports.py?rev=1460013&r1=1460012&r2=1460013&view=diff
==============================================================================
--- qpid/trunk/qpid/python/qpid/messaging/transports.py (original)
+++ qpid/trunk/qpid/python/qpid/messaging/transports.py Fri Mar 22 21:45:42 2013
@@ -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



---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to