diff options
author | Rob Crittenden <rcritten@redhat.com> | 2015-09-25 09:12:00 -0400 |
---|---|---|
committer | Rob Crittenden <rcritten@redhat.com> | 2015-10-02 16:51:56 -0400 |
commit | 15af3be170f05b64a967f4f7755a90608033c374 (patch) | |
tree | d0448ee439e6229580459fc55d532e807856da9b | |
parent | 00fe09480dfd28674661830d8a045e0f560bbe51 (diff) | |
download | mod_nss-15af3be170f05b64a967f4f7755a90608033c374.tar.gz mod_nss-15af3be170f05b64a967f4f7755a90608033c374.tar.xz mod_nss-15af3be170f05b64a967f4f7755a90608033c374.zip |
Add test suite for SNI
python for OpenSSL is in quite a sad state with several competing
mid-level implementations which provide different feature sets.
The httplib client provides access to the negotiated cipher and
protocol but not SNI (and it has lousy hostname checking).
The urllib3 client provides SNI and is generally better but doesn't
give any details on the connection.
So I'm using both. The original one is used for basic server testing
and the urllib3 one is used just for SNI testing.
Also:
- Indent the test configuration to make it more readable
- Add separate config file for SNI testing
- Add a CGI configuration and script to test CGI variables
- Change client cipher test to use AES256-SHA instead of RC4
- Add a commented-out valgrind option in start for future
debuggers
- Change the VirtualServers to *:port and use ServerName
- Add per-VH document roots so SNI can be more easily tested
-rw-r--r-- | Makefile.am | 12 | ||||
-rwxr-xr-x | gencert.in | 71 | ||||
-rw-r--r-- | test/README.sni | 30 | ||||
-rwxr-xr-x | test/createinstance.sh | 74 | ||||
-rwxr-xr-x | test/printenv.pl | 11 | ||||
-rwxr-xr-x | test/setup.sh | 20 | ||||
-rw-r--r-- | test/sni.tmpl | 28 | ||||
-rw-r--r-- | test/suite1.tmpl | 178 | ||||
-rw-r--r-- | test/test.py | 39 | ||||
-rw-r--r-- | test/test_config.py | 41 | ||||
-rw-r--r-- | test/test_request.py | 35 | ||||
-rw-r--r-- | test/testsni.py | 101 |
12 files changed, 510 insertions, 130 deletions
diff --git a/Makefile.am b/Makefile.am index 4978ff3..ca89989 100644 --- a/Makefile.am +++ b/Makefile.am @@ -103,12 +103,20 @@ check: cd test; \ rm -rf work; \ nosetests -v test_cipher.py; \ - ./setup.sh; \ + ./setup.sh -s 1; \ nosetests -v test.py; \ sleep 5; \ rm -rf work; \ - ./setup.sh sql:; \ + ./setup.sh sql: -s 1; \ DBPREFIX=sql: nosetests -v test.py; \ cd .. +checksni: + cd test; \ + rm -rf work; \ + ./setup.sh -s 25; \ + nosetests -v testsni.py; \ + cd .. + + .PHONY: all test clean @@ -61,7 +61,7 @@ ALPHA_CERTDN="E=alpha@${FQDN}, CN=Frank Alpha, UID=alpha, OU=People, O=example.c BETA_CERTDN="E=beta@${FQDN}, CN=Anna Beta, UID=beta, OU=People, O=example.com, C=US" # size of the keys -KEYSIZE=1024 +KEYSIZE=2048 # validity of the certs in months VALIDITY=48 @@ -84,9 +84,65 @@ then fi DBDIR=$1 +shift + +if [ $# > 0 ]; then + SNI=$1 +else + SNI=0 +fi echo "httptest" > $DEST/pw.txt +function generate_server_sni_cert { + hostname=$1 + + local SERVER_DN="CN=${hostname}, O=SNI, O=example.com, C=US" + local NICKNAME="Server-Cert-${hostname}" + + echo "" + echo "#####################################################################" + echo "Generating $NICKNAME server certificate request" + echo "#####################################################################" + (ps -elf; date; netstat -a) > $DEST/noise + $CERTUTIL -R -d $DBDIR \ + -s "$SERVER_DN" \ + -o $DEST/tmpcertreq \ + -g $KEYSIZE \ + -z $DEST/noise \ + -f $DEST/pw.txt + + echo "" + echo "#####################################################################" + echo "Generating $NICKNAME server certificate" + echo "#####################################################################" + let CERTSERIAL=CERTSERIAL+1 + echo -e "2\n9\nn\n1\n9\nn\n" | \ + $CERTUTIL -C -d $DBDIR \ + -c cacert \ + -i $DEST/tmpcertreq \ + -o $DEST/tmpcert.der \ + -m $CERTSERIAL \ + -v $VALIDITY \ + -f $DEST/pw.txt \ + -1 \ + -5 \ + -8 $hostname + + rm $DEST/tmpcertreq + + echo "" + echo "#####################################################################" + echo "Importing $NICKNAME certificate into server cert DB" + echo "#####################################################################" + $CERTUTIL -A -d $DBDIR -n $NICKNAME \ + -t u,u,u \ + -i $DEST/tmpcert.der \ + -f $DEST/pw.txt + + rm $DEST/tmpcert.der +} + echo "" echo "#####################################################################" echo "Generating new server certificate and key database. The password" @@ -115,7 +171,6 @@ $CERTUTIL -S -d $DBDIR -n cacert \ -z $DEST/noise \ -2 \ -1 \ - -5 echo "" echo "#####################################################################" @@ -185,7 +240,8 @@ $CERTUTIL -C -d $DBDIR \ -v $VALIDITY \ -f $DEST/pw.txt \ -1 \ - -5 + -5 \ + -8 $FQDN rm $DEST/tmpcertreq @@ -200,6 +256,15 @@ $CERTUTIL -A -d $DBDIR -n Server-Cert \ rm $DEST/tmpcert.der +if [ $SNI > 0 ]; then + SNI=`expr $SNI + 1` + count=1 + while test $count -lt $SNI ; do + generate_server_sni_cert www$count.example.com + count=`expr $count + 1` + done +fi + echo "" echo "#####################################################################" echo "Cleaning up" diff --git a/test/README.sni b/test/README.sni new file mode 100644 index 0000000..08ea95b --- /dev/null +++ b/test/README.sni @@ -0,0 +1,30 @@ +The SNI tests are overly complicated because of the sad state of affairs +of pyOpenSSL. If I use the SSL client in http.client I can override some +methods and have access to the negotiated protocol and cipher but don't +have access to SNI. + +Or I can use the SSL client in urllib3.contrib and have working SNI but +no access to the negotiated protocol or cipher. + +So I split the baby. When running the existing test suite I use the original +override methods so I can continue to do cipher and protocol negotiation +testing. When running the SNI tests I use the urllib3 SSL client and do +only SNI testing. + +To run the tests: + +You need to edit /etc/hosts and add: + +your_ip_address www[1-25].example.com + +E.g. + +192.168.0.1 www1.example.com +192.168.0.1 www2.example.com +... +192.168.0.1 www25.example.com + +Do not create www26 as that is used as a negative test. + +setup.sh and gencert have been extended to generate a bunch of certs +suitable for SNI testing. diff --git a/test/createinstance.sh b/test/createinstance.sh index 9cdba62..45a8f04 100755 --- a/test/createinstance.sh +++ b/test/createinstance.sh @@ -2,15 +2,45 @@ # # Make a temporary Apache instance for testing. -if [ " $#" -eq 0 ]; then - echo "Usage: $0 /path/to/instance" - exit 1 -fi +function create_content_dirs { + local dir=$1 + + mkdir $1 + + # Create the content + mkdir $dir/rc4_cipher + mkdir $dir/openssl_rc4_cipher + mkdir $dir/openssl_aes_cipher + mkdir $dir/acl + mkdir $dir/protocolssl2 + mkdir $dir/protocolssl3 + mkdir $dir/protocoltls1 + mkdir $dir/protocoltls11 + mkdir $dir/protocoltls12 + + cat > $dir/index.html << EOF + <html> + Basic index page for $dir + </html +EOF + + cp $dir/index.html $dir/acl/aclS01.html + cp $dir/index.html $dir/acl/aclS02.html + cp $dir/index.html $dir/acl/aclS03.html + cp $dir/index.html $dir/secret-test.html + cp $dir/index.html $dir/protocolssl2/index.html + cp $dir/index.html $dir/protocolssl3/index.html + cp $dir/index.html $dir/protocoltls1/index.html + cp $dir/index.html $dir/protocoltls11/index.html + cp $dir/index.html $dir/protocoltls12/index.html +} + target=$1 echo "Creating instance in $target" mkdir -p $target +# Make the default server root cd $target mkdir alias mkdir bin @@ -18,35 +48,18 @@ mkdir conf mkdir conf.d mkdir logs mkdir run -mkdir content mkdir cgi-bin mkdir lib -# Create the content -mkdir content/rc4_cipher -mkdir content/openssl_rc4_cipher -mkdir content/openssl_aes_cipher -mkdir content/acl -mkdir content/protocolssl2 -mkdir content/protocolssl3 -mkdir content/protocoltls1 -mkdir content/protocoltls11 -mkdir content/protocoltls12 - -cat > content/index.html << EOF -<html> -Basic index page -</html -EOF -cp content/index.html content/acl/aclS01.html -cp content/index.html content/acl/aclS02.html -cp content/index.html content/acl/aclS03.html -cp content/index.html content/secret-test.html -cp content/index.html content/protocolssl2/index.html -cp content/index.html content/protocolssl3/index.html -cp content/index.html content/protocoltls1/index.html -cp content/index.html content/protocoltls11/index.html -cp content/index.html content/protocoltls12/index.html +touch conf.d/empty.conf + +# Create the content directories +create_content_dirs content +count=1 +while test $count -lt 26 ; do + create_content_dirs "sni${count}" + count=`expr $count + 1` +done ln -s /etc/httpd/modules modules @@ -60,6 +73,7 @@ EOF cat << EOF > start #!/bin/sh HTTPD=/usr/sbin/httpd +#valgrind --leak-check=full --log-file=valgrind.out --trace-children=yes \$HTTPD -X -k start -d . -f ./conf/httpd.conf \$HTTPD -k start -d . -f ./conf/httpd.conf EOF diff --git a/test/printenv.pl b/test/printenv.pl new file mode 100755 index 0000000..a24e98b --- /dev/null +++ b/test/printenv.pl @@ -0,0 +1,11 @@ +#!/usr/bin/perl + +binmode(STDOUT); +binmode(STDIN); + +print "Content-Type: text/plain\r\n"; +print "\r\n"; + +foreach $key (sort (keys (%ENV))) { + print "$key=$ENV{$key}\n"; +} diff --git a/test/setup.sh b/test/setup.sh index 487a5f5..33cf4f6 100755 --- a/test/setup.sh +++ b/test/setup.sh @@ -5,6 +5,22 @@ server_gid=$USER server_port=8000 server_name=`hostname` +while [[ $# > 1 ]] +do +key="$1" + +case $key in + -s|--sni) + SNI="$2" + shift # past argument + ;; + *) + # unknown option + ;; +esac +shift # past argument or value +done + DBPREFIX=$1 test_root=$currentpath/work/httpd @@ -20,12 +36,14 @@ if [ -e $test_root ]; then fi ./createinstance.sh ${test_root} +cp printenv.pl ${test_root}/cgi-bin +chmod 755 ${test_root}/cgi-bin/printenv.pl cp ../.libs/libmodnss.so ${test_root}/lib cp ../nss_pcache ${test_root}/bin echo "Generating a new certificate database..." -bash ../gencert ${DBPREFIX}${test_root}/alias > /dev/null 2>&1 +bash ../gencert ${DBPREFIX}${test_root}/alias $SNI > /dev/null 2>&1 echo internal:httptest > ${test_root}/conf/password.conf # Export the CA cert diff --git a/test/sni.tmpl b/test/sni.tmpl new file mode 100644 index 0000000..f58e9aa --- /dev/null +++ b/test/sni.tmpl @@ -0,0 +1,28 @@ +<VirtualHost *:8000> + + ServerName $SNINAME + DocumentRoot $SERVER_ROOT/sni$SNINUM + + NSSEngine on + NSSFIPS off + NSSOCSP off + NSSRenegotiation on + + NSSCipherSuite +aes_128_sha_256,+aes_256_sha_256,+rsa_aes_128_gcm_sha_256 + + NSSProtocol TLSv1.2 + + NSSNickname Server-Cert-$SNINAME + + NSSVerifyClient none + + # A bit redundant since the initial handshake should fail if no TLSv1.2 + <Location "/protocoltls12"> + NSSRequire %{SSL_PROTOCOL} eq "TLSv1.2" + </Location> + + <Directory "$SERVER_ROOT/cgi-bin"> + NSSOptions +ExportCertData +CompatEnvVars +StdEnvVars + </Directory> + +</VirtualHost> diff --git a/test/suite1.tmpl b/test/suite1.tmpl index 0f739a2..a069383 100644 --- a/test/suite1.tmpl +++ b/test/suite1.tmpl @@ -12,107 +12,139 @@ Listen 0.0.0.0:8001 LogLevel debug -<VirtualHost $SERVER_NAME:$SERVER_PORT> +<VirtualHost *:$SERVER_PORT> -NSSEngine on -NSSFIPS off -NSSOCSP off -NSSRenegotiation on + ServerName $SERVER_NAME + DocumentRoot $SERVER_ROOT/content -NSSCipherSuite +rsa_rc4_128_md5,+rsa_3des_sha,+rsa_des_sha,+rsa_rc4_40_md5,+rsa_rc2_40_md5,+rsa_null_md5,+rsa_des_56_sha,+rsa_rc4_56_sha,+rsa_aes_128_sha,+rsa_aes_256_sha + NSSSNI $SNI + NSSEngine on + NSSFIPS off + NSSOCSP off + NSSRenegotiation on -NSSProtocol SSLv3,TLSv1.0 + NSSCipherSuite +rsa_rc4_128_md5,+rsa_3des_sha,+rsa_des_sha,+rsa_aes_128_sha,+rsa_aes_256_sha -NSSNickname Server-Cert + NSSProtocol SSLv3,TLSv1.0 -NSSCertificateDatabase $DBPREFIX$SERVER_ROOT/alias + NSSNickname Server-Cert -NSSVerifyClient none + NSSCertificateDatabase $DBPREFIX$SERVER_ROOT/alias -NSSUserName SSL_CLIENT_S_DN_UID + NSSVerifyClient none -<Location "/rc4_cipher"> - NSSCipherSuite +rsa_rc4_128_md5 -</Location> + NSSUserName SSL_CLIENT_S_DN_UID -<Location "/openssl_rc4_cipher"> - NSSCipherSuite RC4-SHA -</Location> + <Location "/rc4_cipher"> + NSSCipherSuite +rsa_rc4_128_md5 + </Location> -<Location "/openssl_aes_cipher"> - # In openssl equivalent of AES:-ECDH:-ADH:-PSK:-DH - # In NSS equivalent of AES:-ECDH - NSSCipherSuite AES+RSA -</Location> + <Location "/openssl_rc4_cipher"> + NSSCipherSuite RC4-SHA + </Location> -<Location "/acl/aclS01.html"> - NSSOptions +StdEnvVars +CompatEnvVars +ExportCertData - NSSVerifyClient require -</Location> + <Location "/openssl_aes_cipher"> + # In openssl equivalent of AES:-ECDH:-ADH:-PSK:-DH + # In NSS equivalent of AES:-ECDH + NSSCipherSuite AES+RSA + </Location> -<Location "/acl/aclS02.html"> - NSSOptions +StdEnvVars +CompatEnvVars +ExportCertData - NSSVerifyClient require - NSSRequire ( %{SSL_CLIENT_S_DN_UID} eq "alpha" \ - or %{SSL_CLIENT_S_DN_UID} eq "gamma" ) \ - and %{SSL_CLIENT_S_DN_O} eq "example.com" \ - and %{SSL_CLIENT_S_DN_OU} eq "People" -</Location> + <Location "/acl/aclS01.html"> + NSSOptions +StdEnvVars +CompatEnvVars +ExportCertData + NSSVerifyClient require + </Location> -<Location "/acl/aclS03.html"> - NSSOptions +StdEnvVars +CompatEnvVars +ExportCertData +FakeBasicAuth - NSSVerifyClient require - AuthType Basic - AuthName Cert - AuthUserFile conf/htpasswd - Require valid-user -</Location> + <Location "/acl/aclS02.html"> + NSSOptions +StdEnvVars +CompatEnvVars +ExportCertData + NSSVerifyClient require + NSSRequire ( %{SSL_CLIENT_S_DN_UID} eq "alpha" \ + or %{SSL_CLIENT_S_DN_UID} eq "gamma" ) \ + and %{SSL_CLIENT_S_DN_O} eq "example.com" \ + and %{SSL_CLIENT_S_DN_OU} eq "People" + </Location> -<Location "/secret-test.html"> - NSSRequire %{SSL_CIPHER_USEKEYSIZE} > 40 -</Location> + <Location "/acl/aclS03.html"> + NSSOptions +StdEnvVars +CompatEnvVars +ExportCertData +FakeBasicAuth + NSSVerifyClient require + AuthType Basic + AuthName Cert + AuthUserFile conf/htpasswd + Require valid-user + </Location> -<Location "/secret-test-impossible.html"> - NSSRequire %{SSL_CIPHER_USEKEYSIZE} > 4000 -</Location> + <Location "/secret-test.html"> + NSSRequire %{SSL_CIPHER_USEKEYSIZE} > 40 + </Location> -<Location "/protocolssl3"> - NSSRequire %{SSL_PROTOCOL} eq "SSLv3" -</Location> + <Location "/secret-test-impossible.html"> + NSSRequire %{SSL_CIPHER_USEKEYSIZE} > 4000 + </Location> -<Location "/protocoltls1"> - NSSRequire %{SSL_PROTOCOL} eq "TLSv1" -</Location> + <Location "/protocolssl3"> + NSSRequire %{SSL_PROTOCOL} eq "SSLv3" + </Location> -<Location "/protocoltls11"> - NSSRequire %{SSL_PROTOCOL} eq "TLSv1.1" -</Location> + <Location "/protocoltls1"> + NSSRequire %{SSL_PROTOCOL} eq "TLSv1" + </Location> + + <Location "/protocoltls11"> + NSSRequire %{SSL_PROTOCOL} eq "TLSv1.1" + </Location> + + <Location "/protocoltls12"> + NSSRequire %{SSL_PROTOCOL} eq "TLSv1.2" + </Location> + + <Directory "$SERVER_ROOT/cgi-bin"> + NSSOptions +ExportCertData +CompatEnvVars +StdEnvVars + </Directory> -<Location "/protocoltls12"> - NSSRequire %{SSL_PROTOCOL} eq "TLSv1.2" -</Location> </VirtualHost> # # For testing protocol handling # -<VirtualHost $SERVER_NAME:8001> +<VirtualHost *:8001> + + ServerName $SERVER_NAME + DocumentRoot $SERVER_ROOT/content -NSSEngine on -NSSFIPS off -NSSOCSP off -NSSRenegotiation on + NSSEngine on + NSSFIPS off + NSSOCSP off + NSSRenegotiation on -NSSCipherSuite +rsa_rc4_128_md5,+rsa_3des_sha,+rsa_des_sha,+rsa_rc4_40_md5,+rsa_rc2_40_md5,+rsa_null_md5,+rsa_des_56_sha,+rsa_rc4_56_sha,+rsa_aes_128_sha,+rsa_aes_256_sha + NSSCipherSuite +aes_128_sha_256,+aes_256_sha_256,+rsa_aes_128_gcm_sha_256 -NSSProtocol TLSv1.2 + NSSProtocol TLSv1.2 -NSSNickname Server-Cert + NSSNickname Server-Cert -NSSVerifyClient none + NSSVerifyClient none + + # A bit redundant since the initial handshake should fail if no TLSv1.2 + <Location "/protocoltls12"> + NSSRequire %{SSL_PROTOCOL} eq "TLSv1.2" + </Location> + + <Directory "$SERVER_ROOT/cgi-bin"> + NSSOptions +ExportCertData +CompatEnvVars +StdEnvVars + </Directory> -# A bit redundant since the initial handshake should fail if no TLSv1.2 -<Location "/protocoltls12"> - NSSRequire %{SSL_PROTOCOL} eq "TLSv1.2" -</Location> </VirtualHost> + +# +# SNI testing. Requires that you add an entry like this to /etc/hosts: +# +# <your_IP> www1.example.com +# +# 25 of these are needed +# +# Test with something like: +# curl --cacert alias/ca.pem -v https://www1.example.com:8000/index.html +# +# Output should be something like: Basic index page for sni1 +# + +include conf.d/* diff --git a/test/test.py b/test/test.py index 23e093c..721d210 100644 --- a/test/test.py +++ b/test/test.py @@ -4,10 +4,31 @@ import ssl import requests.exceptions import os +try: + # python3.2+ + from ssl import CertificateError +except ImportError: + try: + # Older python where the backport from pypi is installed + from backports.ssl_match_hostname import CertificateError + except ImportError: + # Other older python we use the urllib3 bundled copy + from urllib3.packages.ssl_match_hostname import CertificateError + class test_suite1(Declarative): @classmethod def setUpClass(cls): - write_template_file('suite1.tmpl', 'work/httpd/conf/test.conf', {'DBPREFIX': os.environ.get('DBPREFIX', '')}) + write_template_file('suite1.tmpl', 'work/httpd/conf/test.conf', + {'DBPREFIX': os.environ.get('DBPREFIX', ''), + 'SNI': 'off'} + ) + # Generate a single VH to do negative SNI testing + write_template_file('sni.tmpl', 'work/httpd/conf.d/sni1.conf', + {'DBPREFIX': os.environ.get('DBPREFIX', ''), + 'SNINAME': 'www1.example.com', + 'SNINUM': 1, + } + ) restart_apache() @classmethod @@ -32,6 +53,7 @@ class test_suite1(Declarative): desc='SSL connection, fail to verify', request=('/', {'verify': True}), expected=requests.exceptions.SSLError(), + expected_str='certificate verify failed', ), dict( @@ -56,10 +78,10 @@ class test_suite1(Declarative): ), dict( - desc='client-side RC4 cipher check', - request=('/', {'ciphers': 'RC4-MD5'}), + desc='client-side cipher check', + request=('/', {'ciphers': 'AES256-SHA'}), expected=200, - cipher='RC4-MD5', + cipher='AES256-SHA', ), dict( @@ -237,4 +259,13 @@ class test_suite1(Declarative): expected=requests.exceptions.SSLError(), ), + dict( + desc='Make non-SNI request', + request=('/index.html', + {'host': 'www1.example.com', 'port': 8000} + ), + expected=requests.exceptions.SSLError(), + expected_str='doesn\'t match', + ), + ] diff --git a/test/test_config.py b/test/test_config.py index 24a2f2a..31c59a1 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -136,21 +136,31 @@ class Declarative(object): session.mount('https://', test_request.MyAdapter()) verify = dict(verify = options) port = options.get('port', DEF_PORT) - request = session.get('https://%s:%d%s' % (FQDN, port, uri), **verify) + host = options.get('host', FQDN) + request = session.get('https://%s:%d%s' % (host, port, uri), **verify) return request - def check(self, nice, desc, request, expected, cipher=None, protocol=None): + def check(self, nice, desc, request, expected, cipher=None, protocol=None, + expected_str=None, content=None): # TODO: need way to set auth, etc. (uri, options) = request if not 'verify' in options: options['verify'] = 'work/httpd/alias/ca.pem' if isinstance(expected, Exception): - self.check_exception(nice, uri, options, expected) + self.check_exception(nice, uri, options, expected, expected_str) else: - self.check_result(nice, uri, options, expected, cipher, protocol) + self.check_result(nice, uri, options, expected, cipher, protocol, + content) - def check_exception(self, nice, uri, options, expected): + def check_exception(self, nice, uri, options, expected, expected_str): + """ + With some requests we expect an exception to be raised. See if + is the one we expect. There is at least one case where depending + on the distro either a CertificateError or an SSLError is + thrown but the message of the exception is the same, so compare + that. + """ klass = expected.__class__ name = klass.__name__ try: @@ -162,15 +172,24 @@ class Declarative(object): EXPECTED % (uri, name, options, output) ) if not isinstance(e, klass): - raise AssertionError( - UNEXPECTED % (uri, name, options, e.__class__.__name__, e) - ) - + if expected_str not in str(e): + raise AssertionError( + UNEXPECTED % (uri, name, options, e.__class__.__name__, e) + ) - def check_result(self, nice, uri, options, expected, cipher=None, protocol=None): + def check_result(self, nice, uri, options, expected, cipher=None, + protocol=None, content=None): name = expected.__class__.__name__ request = self.make_request(uri, options) + has_sni = options.get('sni', False) + + if content and not content in request.content: + raise AssertionError( + 'Expected %s not in %s' % (content, request.content) + ) if cipher: + if has_sni: + raise AssertionError('Cannot do cipher tests in SNI') client_cipher = request.raw._pool._get_conn().client_cipher if cipher != client_cipher[0]: raise AssertionError( @@ -178,6 +197,8 @@ class Declarative(object): ) if protocol: client_cipher = request.raw._pool._get_conn().client_cipher + if has_sni: + raise AssertionError('Cannot do protocol tests in SNI') if protocol != client_cipher[1]: raise AssertionError( 'Expected protocol %s, got %s' % (protocol, client_cipher[1]) diff --git a/test/test_request.py b/test/test_request.py index bac2a2d..5d2a525 100644 --- a/test/test_request.py +++ b/test/test_request.py @@ -4,9 +4,12 @@ import socket import requests import urlparse -from urllib3.util import get_host -from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool import logging +import socket +from requests.packages.urllib3.util import get_host +from requests.packages.urllib3.util.timeout import Timeout +from requests.packages.urllib3.contrib import pyopenssl +from requests.packages.urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool, VerifiedHTTPSConnection # Don't bend over backwards for ssl support, assume it is there. import ssl @@ -30,6 +33,8 @@ except ImportError: # Other older python we use the urllib3 bundled copy from urllib3.packages.ssl_match_hostname import match_hostname, CertificateError +SAVE_DEFAULT_SSL_CIPHER_LIST = pyopenssl.DEFAULT_SSL_CIPHER_LIST + log = logging.getLogger(__name__) @@ -61,7 +66,7 @@ def connection_from_url(url, **kw): class MyHTTPSConnectionPool(HTTPSConnectionPool): def __init__(self, host, port=None, - strict=False, timeout=None, maxsize=1, + strict=False, timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, block=False, headers=None, key_file=None, cert_file=None, cert_reqs='CERT_REQUIRED', ca_certs='/etc/ssl/certs/ca-certificates.crt', ssl_version=ssl.PROTOCOL_SSLv23, ciphers=None): @@ -75,6 +80,8 @@ class MyHTTPSConnectionPool(HTTPSConnectionPool): self.ca_certs = ca_certs self.ssl_version = ssl_version self.ciphers = ciphers + self.assert_hostname = None + self.assert_fingerprint = None def _new_conn(self): """ @@ -92,13 +99,14 @@ class MyHTTPSConnectionPool(HTTPSConnectionPool): # return HTTPSConnection(host=self.host, port=self.port) connection = MyVerifiedHTTPSConnection(host=self.host, port=self.port) + connection.sni = self.sni connection.set_cert(key_file=self.key_file, cert_file=self.cert_file, cert_reqs=self.cert_reqs, ca_certs=self.ca_certs) connection.set_ssl_version(self.ssl_version) connection.set_ciphers(self.ciphers) return connection -class MyVerifiedHTTPSConnection(HTTPSConnection): +class MyVerifiedHTTPSConnection(VerifiedHTTPSConnection): """ Based on httplib.HTTPSConnection but wraps the socket with SSL certification. @@ -106,6 +114,10 @@ class MyVerifiedHTTPSConnection(HTTPSConnection): cert_reqs = None ca_certs = None client_cipher = None + is_verified = True # squelch warning + sni = False + assert_hostname = None + assert_fingerprint = None def set_cert(self, key_file=None, cert_file=None, cert_reqs='CERT_NONE', ca_certs=None): @@ -127,6 +139,13 @@ class MyVerifiedHTTPSConnection(HTTPSConnection): self.ciphers = ciphers def connect(self): + if self.sni: + if self.ciphers: + pyopenssl.DEFAULT_SSL_CIPHER_LIST = self.ciphers + else: + pyopenssl.DEFAULT_SSL_CIPHER_LIST = SAVE_DEFAULT_SSL_CIPHER_LIST + return super(MyVerifiedHTTPSConnection, self).connect() + # Add certificate verification sock = socket.create_connection((self.host, self.port), self.timeout) @@ -141,9 +160,10 @@ class MyVerifiedHTTPSConnection(HTTPSConnection): match_hostname(self.sock.getpeercert(), self.host) def close(self): - if self.sock: - self.client_cipher = self.sock.cipher() - HTTPSConnection.close(self) + if not self.sni: + if self.sock: + self.client_cipher = self.sock.cipher() + super(MyVerifiedHTTPSConnection, self).close() class MyAdapter(requests.adapters.HTTPAdapter): @@ -171,6 +191,7 @@ class MyAdapter(requests.adapters.HTTPAdapter): conn.cert_file = verify['cert_file'] if 'key_file' in verify: conn.key_file = verify['key_file'] + conn.sni = verify.get('sni', False) else: # huh? Do nothing pass diff --git a/test/testsni.py b/test/testsni.py new file mode 100644 index 0000000..9808f91 --- /dev/null +++ b/test/testsni.py @@ -0,0 +1,101 @@ +from test_config import Declarative, write_template_file, restart_apache +from test_config import stop_apache +import ssl +import requests.exceptions +import os + +class test_suite1(Declarative): + @classmethod + def setUpClass(cls): + write_template_file('suite1.tmpl', 'work/httpd/conf/test.conf', + {'DBPREFIX': os.environ.get('DBPREFIX', ''), + 'SNI': 'on'} + ) + for i in range(1,26): + write_template_file('sni.tmpl', 'work/httpd/conf.d/sni%d.conf' % i, + {'DBPREFIX': os.environ.get('DBPREFIX', ''), + 'SNINAME': 'www%d.example.com' % i, + 'SNINUM': i, + } + ) + restart_apache() + + @classmethod + def tearDownClass(cls): + stop_apache() + + tests = [ + + dict( + desc='Get this host', + request=('/', {'sni': True}), + expected=200, + content='content', + ), + + dict( + desc='Get www1.example.com', + request=('/', {'host': 'www1.example.com', 'sni': True}), + expected=200, + content='sni1', + ), + + dict( + desc='Get www2.example.com', + request=('/', {'host': 'www2.example.com', 'sni': True}), + expected=200, + content='sni2', + ), + + dict( + desc='Get www4.example.com', + request=('/', {'host': 'www4.example.com', 'sni': True}), + expected=200, + content='sni4', + ), + + dict( + desc='Get www6.example.com', + request=('/', {'host': 'www6.example.com', 'sni': True}), + expected=200, + content='sni6', + ), + + dict( + desc='Get www1.example.com again', + request=('/', {'host': 'www1.example.com', 'sni': True}), + expected=200, + content='sni1', + ), + + dict( + desc='Get non-existant page on www8.example.com', + request=('/notfound', {'host': 'www8.example.com', 'sni': True}), + expected=404, + ), + + dict( + desc='Client auth to www10.example.com, valid certificate', + request=('/acl/aclS01.html', { + 'host': 'www10.example.com', 'sni': True, + 'key_file': 'work/httpd/alpha.key', + 'cert_file': 'work/httpd/alpha.crt',} + ), + expected=200, + content='sni10', + ), + + dict( + desc='Get www25.example.com', + request=('/', {'host': 'www25.example.com', 'sni': True}), + expected=200, + content='sni25', + ), + + dict( + desc='Non-existant www26.example.com', + request=('/', {'host': 'www26.example.com', 'sni': True}), + expected=requests.exceptions.ConnectionError(), + ), + + ] |