diff options
-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(), + ), + + ] |