summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRob Crittenden <rcritten@redhat.com>2015-09-25 09:12:00 -0400
committerRob Crittenden <rcritten@redhat.com>2015-10-02 16:51:56 -0400
commit15af3be170f05b64a967f4f7755a90608033c374 (patch)
treed0448ee439e6229580459fc55d532e807856da9b
parent00fe09480dfd28674661830d8a045e0f560bbe51 (diff)
downloadmod_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.am12
-rwxr-xr-xgencert.in71
-rw-r--r--test/README.sni30
-rwxr-xr-xtest/createinstance.sh74
-rwxr-xr-xtest/printenv.pl11
-rwxr-xr-xtest/setup.sh20
-rw-r--r--test/sni.tmpl28
-rw-r--r--test/suite1.tmpl178
-rw-r--r--test/test.py39
-rw-r--r--test/test_config.py41
-rw-r--r--test/test_request.py35
-rw-r--r--test/testsni.py101
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
diff --git a/gencert.in b/gencert.in
index ffccaab..1b76e1f 100755
--- a/gencert.in
+++ b/gencert.in
@@ -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(),
+ ),
+
+ ]