Browse Source

== Release 0.2.0 ==
* added support for SSL verification with subjectAltNames using pyasn1
* fixed minor bug - SSL cert DN prefix matching

git-svn-id: http://proj.badc.rl.ac.uk/svn/ndg-security/trunk/ndg_httpsclient@8012 051b1e3e-aa0c-0410-b6c2-bfbade6052be

pjkersha 13 years ago
parent
commit
32902723fd

+ 63 - 2
ndg/httpsclient/ssl_peer_verification.py

@@ -10,6 +10,12 @@ import re
 import logging
 import logging
 log = logging.getLogger(__name__)
 log = logging.getLogger(__name__)
 
 
+try:
+    from ndg.httpsclient.subj_alt_name import SubjectAltName
+    from pyasn1.codec.der import decoder as der_decoder
+    subj_alt_name_support = True
+except ImportError, e:
+    subj_alt_name_support = False
 
 
 class ServerSSLCertVerification(object):
 class ServerSSLCertVerification(object):
     """Check server identity.  If hostname doesn't match, allow match of
     """Check server identity.  If hostname doesn't match, allow match of
@@ -26,12 +32,13 @@ class ServerSSLCertVerification(object):
         'domainComponent':          'DC',
         'domainComponent':          'DC',
         'userid':                   'UID'
         'userid':                   'UID'
     }
     }
+    SUBJ_ALT_NAME_EXT_NAME = 'subjectAltName'
     PARSER_RE_STR = '/(%s)=' % '|'.join(DN_LUT.keys() + DN_LUT.values())
     PARSER_RE_STR = '/(%s)=' % '|'.join(DN_LUT.keys() + DN_LUT.values())
     PARSER_RE = re.compile(PARSER_RE_STR)
     PARSER_RE = re.compile(PARSER_RE_STR)
 
 
-    __slots__ = ('__hostname', '__certDN')
+    __slots__ = ('__hostname', '__certDN', '__subj_alt_name_match')
 
 
-    def __init__(self, certDN=None, hostname=None):
+    def __init__(self, certDN=None, hostname=None, subj_alt_name_match=True):
         """Override parent class __init__ to enable setting of certDN
         """Override parent class __init__ to enable setting of certDN
         setting
         setting
 
 
@@ -39,6 +46,13 @@ class ServerSSLCertVerification(object):
         @param certDN: Set the expected Distinguished Name of the
         @param certDN: Set the expected Distinguished Name of the
         server to avoid errors matching hostnames.  This is useful
         server to avoid errors matching hostnames.  This is useful
         where the hostname is not fully qualified
         where the hostname is not fully qualified
+        @type hostname: string
+        @param hostname: hostname to match against peer certificate 
+        subjectAltNames or subject common name
+        @type subj_alt_name_match: bool
+        @param subj_alt_name_match: flag to enable/disable matching of hostname
+        against peer certificate subjectAltNames.  Nb. A setting of True will 
+        be ignored if the pyasn1 package is not installed
         """
         """
         self.__certDN = None
         self.__certDN = None
         self.__hostname = None
         self.__hostname = None
@@ -48,6 +62,18 @@ class ServerSSLCertVerification(object):
 
 
         if hostname is not None:
         if hostname is not None:
             self.hostname = hostname
             self.hostname = hostname
+        
+        if subj_alt_name_match:
+            if not subj_alt_name_support:
+                log.warning('Overriding "subj_alt_name_match" keyword setting: '
+                            'peer verification with subjectAltNames is disabled')
+                self.__subj_alt_name_match = False
+                
+            self.__subj_alt_name_match = True
+        else:
+            log.debug('Disabling peer verification with subject '
+                      'subjectAltNames!')
+            self.__subj_alt_name_match = False
 
 
     def __call__(self, connection, peerCert, errorStatus, errorDepth,
     def __call__(self, connection, peerCert, errorStatus, errorDepth,
                  preverifyOK):
                  preverifyOK):
@@ -98,6 +124,13 @@ class ServerSSLCertVerification(object):
                               'certificate against')
                               'certificate against')
                     return False
                     return False
 
 
+                # Check for subject alternative names
+                if self.__subj_alt_name_match:
+                    dns_names = self._get_subj_alt_name(peerCert)
+                    if self.hostname in dns_names:
+                        return preverifyOK
+                
+                # If no subjectAltNames, default to check of subject Common Name   
                 if peerCertSubj.commonName == self.hostname:
                 if peerCertSubj.commonName == self.hostname:
                     return preverifyOK
                     return preverifyOK
                 else:
                 else:
@@ -115,6 +148,34 @@ class ServerSSLCertVerification(object):
         else:
         else:
             return preverifyOK
             return preverifyOK
 
 
+    @classmethod
+    def _get_subj_alt_name(cls, peer_cert):
+        '''Extract subjectAltName DNS name settings from certificate extensions
+        
+        @param peer_cert: peer certificate in SSL connection.  subjectAltName
+        settings if any will be extracted from this
+        @type peer_cert: OpenSSL.crypto.X509
+        '''
+        # Search through extensions
+        dns_name = []
+        general_names = SubjectAltName()
+        for i in range(peer_cert.get_extension_count()):
+            ext = peer_cert.get_extension(i)
+            ext_name = ext.get_short_name()
+            if ext_name == cls.SUBJ_ALT_NAME_EXT_NAME:
+                # PyOpenSSL returns extension data in ASN.1 encoded form
+                ext_dat = ext.get_data()
+                decoded_dat = der_decoder.decode(ext_dat, 
+                                                 asn1Spec=general_names)
+                
+                for name in decoded_dat:
+                    if isinstance(name, SubjectAltName):
+                        for entry in range(len(name)):
+                            component = name.getComponentByPosition(entry)
+                            dns_name.append(str(component.getComponent()))
+                            
+        return dns_name
+        
     def _getCertDN(self):
     def _getCertDN(self):
         return self.__certDN
         return self.__certDN
 
 

+ 144 - 0
ndg/httpsclient/subj_alt_name.py

@@ -0,0 +1,144 @@
+"""NDG HTTPS Client package
+
+Use pyasn1 to provide support for parsing ASN.1 formatted subjectAltName
+content for SSL peer verification.  Code based on:
+
+http://stackoverflow.com/questions/5519958/how-do-i-parse-subjectaltname-extension-data-using-pyasn1
+"""
+__author__ = "P J Kershaw"
+__date__ = "01/02/12"
+__copyright__ = "(C) 2012 Science and Technology Facilities Council"
+__license__ = "BSD - see LICENSE file in top-level directory"
+__contact__ = "Philip.Kershaw@stfc.ac.uk"
+__revision__ = '$Id$'
+try:
+    from pyasn1.type import univ, constraint, char, namedtype, tag
+    
+except ImportError, e:
+    import_error_msg = ('Error importing pyasn1, subjectAltName check for SSL '
+                        'peer verification will be disabled.  Import error '
+                        'is: %s' % e)
+    import warnings
+    warnings.warn(import_error_msg)
+    class Pyasn1ImportError(ImportError):
+        "Raise for pyasn1 import error"
+    raise Pyasn1ImportError(import_error_msg)
+    
+    
+MAX = 64
+
+
+class DirectoryString(univ.Choice):
+    """ASN.1 Directory string class"""
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType(
+            'teletexString', char.TeletexString().subtype(
+                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
+        namedtype.NamedType(
+            'printableString', char.PrintableString().subtype(
+                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
+        namedtype.NamedType(
+            'universalString', char.UniversalString().subtype(
+                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
+        namedtype.NamedType(
+            'utf8String', char.UTF8String().subtype(
+                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
+        namedtype.NamedType(
+            'bmpString', char.BMPString().subtype(
+                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
+        namedtype.NamedType(
+            'ia5String', char.IA5String().subtype(
+                subtypeSpec=constraint.ValueSizeConstraint(1, MAX))),
+        )
+
+
+class AttributeValue(DirectoryString):
+    """ASN.1 Attribute value"""
+
+
+class AttributeType(univ.ObjectIdentifier):
+    """ASN.1 Attribute type"""
+
+
+class AttributeTypeAndValue(univ.Sequence):
+    """ASN.1 Attribute type and value class"""
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('type', AttributeType()),
+        namedtype.NamedType('value', AttributeValue()),
+        )
+
+
+class RelativeDistinguishedName(univ.SetOf):
+    '''ASN.1 Realtive distinguished name'''
+    componentType = AttributeTypeAndValue()
+
+class RDNSequence(univ.SequenceOf):
+    '''ASN.1 RDN sequence class'''
+    componentType = RelativeDistinguishedName()
+
+
+class Name(univ.Choice):
+    '''ASN.1 name class'''
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('', RDNSequence()),
+        )
+
+
+class Extension(univ.Sequence):
+    '''ASN.1 extension class'''
+    componentType = namedtype.NamedTypes(
+        namedtype.NamedType('extnID', univ.ObjectIdentifier()),
+        namedtype.DefaultedNamedType('critical', univ.Boolean('False')),
+        namedtype.NamedType('extnValue', univ.OctetString()),
+        )
+
+
+class Extensions(univ.SequenceOf):
+    '''ASN.1 extensions class'''
+    componentType = Extension()
+    sizeSpec = univ.SequenceOf.sizeSpec + constraint.ValueSizeConstraint(1, MAX)
+
+
+class GeneralName(univ.Choice):
+    '''ASN.1 configuration for X.509 certificate subjectAltNames fields'''
+    componentType = namedtype.NamedTypes(
+#        namedtype.NamedType('otherName', AnotherName().subtype(
+#                            implicitTag=tag.Tag(tag.tagClassContext,
+#                                                tag.tagFormatSimple, 0))),
+        namedtype.NamedType('rfc822Name', char.IA5String().subtype(
+                            implicitTag=tag.Tag(tag.tagClassContext,
+                                                tag.tagFormatSimple, 1))),
+        namedtype.NamedType('dNSName', char.IA5String().subtype(
+                            implicitTag=tag.Tag(tag.tagClassContext,
+                                                tag.tagFormatSimple, 2))),
+#        namedtype.NamedType('x400Address', ORAddress().subtype(
+#                            implicitTag=tag.Tag(tag.tagClassContext,
+#                                                tag.tagFormatSimple, 3))),
+        namedtype.NamedType('directoryName', Name().subtype(
+                            implicitTag=tag.Tag(tag.tagClassContext,
+                                                tag.tagFormatSimple, 4))),
+#        namedtype.NamedType('ediPartyName', EDIPartyName().subtype(
+#                            implicitTag=tag.Tag(tag.tagClassContext,
+#                                                tag.tagFormatSimple, 5))),
+        namedtype.NamedType('uniformResourceIdentifier', char.IA5String().subtype(
+                            implicitTag=tag.Tag(tag.tagClassContext,
+                                                tag.tagFormatSimple, 6))),
+        namedtype.NamedType('iPAddress', univ.OctetString().subtype(
+                            implicitTag=tag.Tag(tag.tagClassContext,
+                                                tag.tagFormatSimple, 7))),
+        namedtype.NamedType('registeredID', univ.ObjectIdentifier().subtype(
+                            implicitTag=tag.Tag(tag.tagClassContext,
+                                                tag.tagFormatSimple, 8))),
+        )
+
+
+class GeneralNames(univ.SequenceOf):
+    '''Sequence of names for ASN.1 subjectAltNames settings'''
+    componentType = GeneralName()
+    sizeSpec = univ.SequenceOf.sizeSpec + constraint.ValueSizeConstraint(1, MAX)
+
+
+class SubjectAltName(GeneralNames):
+    '''ASN.1 implementation for subjectAltNames support'''
+
+

+ 12 - 59
ndg/httpsclient/test/pki/localhost.crt

@@ -1,61 +1,14 @@
-Certificate:
-    Data:
-        Version: 3 (0x2)
-        Serial Number: 40 (0x28)
-        Signature Algorithm: md5WithRSAEncryption
-        Issuer: O=NDG, OU=Security, CN=Test CA
-        Validity
-            Not Before: Jul  6 11:32:04 2011 GMT
-            Not After : Jul  5 11:32:04 2012 GMT
-        Subject: O=NDG, OU=Security, CN=localhost
-        Subject Public Key Info:
-            Public Key Algorithm: rsaEncryption
-            RSA Public Key: (2048 bit)
-                Modulus (2048 bit):
-                    00:c0:e0:94:c2:e9:c0:df:96:36:ba:4d:0e:3f:bc:
-                    41:51:7b:4f:fe:d8:82:47:52:f8:36:57:35:15:3b:
-                    83:77:ba:84:aa:a3:48:f0:03:0b:5a:b7:31:40:8d:
-                    3f:87:05:9e:06:c6:72:1f:ca:7d:ed:73:3c:d0:76:
-                    4d:3b:32:89:e7:1f:5b:84:27:bf:8b:72:09:2a:d4:
-                    40:8d:2c:c4:c3:23:68:6c:f4:62:55:a8:e0:2e:8a:
-                    c8:b4:5f:bf:e1:18:d7:6a:a6:1a:90:6d:e6:83:17:
-                    10:3a:95:b5:da:0d:44:3f:df:fb:6c:c5:9e:bf:1d:
-                    04:5f:1c:16:ce:9e:f7:9d:81:f8:fe:ca:43:82:5f:
-                    6b:c8:70:17:f8:e8:f3:74:d9:fb:ac:14:af:99:8f:
-                    fd:f3:ad:e2:7e:8d:f3:bd:89:15:f0:39:ea:51:7c:
-                    29:81:8c:bc:ba:63:6b:69:e0:c1:46:2f:27:93:83:
-                    85:be:8b:bb:aa:b8:76:c3:ec:8a:f6:50:e1:c3:90:
-                    9e:47:1d:19:68:40:62:59:13:8b:eb:e8:89:64:20:
-                    ec:a3:b7:e7:b2:8f:98:f2:64:b4:aa:6e:d0:f1:73:
-                    fc:ea:ed:19:19:67:98:11:f5:95:ca:76:0b:c7:43:
-                    35:3c:53:23:b5:67:b0:b5:26:59:d1:c5:3e:4a:d1:
-                    70:dd
-                Exponent: 65537 (0x10001)
-        X509v3 extensions:
-            Netscape Cert Type: 
-                SSL Client, SSL Server, S/MIME, Object Signing
-    Signature Algorithm: md5WithRSAEncryption
-        5a:2d:a4:5d:b2:b6:37:60:ed:16:62:88:a7:26:b7:d6:10:b4:
-        d0:f1:25:08:5f:b3:54:34:14:1b:3e:4e:b7:17:a5:6e:54:42:
-        a0:99:0b:41:ad:dc:e9:59:0f:c1:19:db:54:ba:a8:c8:09:44:
-        43:37:62:f9:59:7f:6a:e6:ff:db:4c:7c:68:d5:e8:0d:2f:58:
-        a9:64:a6:57:82:45:d1:41:a9:38:cb:29:70:13:eb:ac:1f:1a:
-        46:b6:b4:bb:a0:ed:0d:12:7c:3e:10:21:a3:62:1d:e2:ec:9c:
-        3a:a0:ca:ce:d2:ed:4a:61:14:19:c2:b5:bd:b6:4d:29:cd:3d:
-        8d:18
 -----BEGIN CERTIFICATE-----
 -----BEGIN CERTIFICATE-----
-MIICdzCCAeCgAwIBAgIBKDANBgkqhkiG9w0BAQQFADAzMQwwCgYDVQQKEwNOREcx
+MIICFjCCAX+gAwIBAgIBCjANBgkqhkiG9w0BAQQFADAzMQwwCgYDVQQKEwNOREcx
-ETAPBgNVBAsTCFNlY3VyaXR5MRAwDgYDVQQDEwdUZXN0IENBMB4XDTExMDcwNjEx
+ETAPBgNVBAsTCFNlY3VyaXR5MRAwDgYDVQQDEwdUZXN0IENBMB4XDTEyMDIwODE2
-MzIwNFoXDTEyMDcwNTExMzIwNFowNTEMMAoGA1UEChMDTkRHMREwDwYDVQQLEwhT
+MTE1M1oXDTE3MDIwNjE2MTE1M1owNTERMA8GA1UECxMIU2VjdXJpdHkxEjAQBgNV
-ZWN1cml0eTESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOC
+BAMTCWxvY2FsaG9zdDEMMAoGA1UEChMDTkRHMIGfMA0GCSqGSIb3DQEBAQUAA4GN
-AQ8AMIIBCgKCAQEAwOCUwunA35Y2uk0OP7xBUXtP/tiCR1L4Nlc1FTuDd7qEqqNI
+ADCBiQKBgQCdhZgzD0xusZqzdphETJPgb4QK/sdDpF8EOT/20bAuyRgGt7papJmc
-8AMLWrcxQI0/hwWeBsZyH8p97XM80HZNOzKJ5x9bhCe/i3IJKtRAjSzEwyNobPRi
+6UtdgS5b9bGh6sRXx+vSKiTqq1ZFLOjnn3OQKhdrK2VU8XiD5rjuwTuNzser0uba
-VajgLorItF+/4RjXaqYakG3mgxcQOpW12g1EP9/7bMWevx0EXxwWzp73nYH4/spD
+lTOW5/2yVab+uZ/vw4yxR64+KdyBuVopXV9STuh12Q0JSrXzdH82iQIDAQABozgw
-gl9ryHAX+OjzdNn7rBSvmY/9863ifo3zvYkV8DnqUXwpgYy8umNraeDBRi8nk4OF
+NjAMBgNVHRMBAf8EAjAAMCYGA1UdEQQfMB2CCWxvY2FsaG9zdIIQbG9jYWxob3N0
-vou7qrh2w+yK9lDhw5CeRx0ZaEBiWROL6+iJZCDso7fnso+Y8mS0qm7Q8XP86u0Z
+LmRvbWFpbjANBgkqhkiG9w0BAQQFAAOBgQBAAQCTkLfgYAjvm63KRXcE8djkYIVQ
-GWeYEfWVynYLx0M1PFMjtWewtSZZ0cU+StFw3QIDAQABoxUwEzARBglghkgBhvhC
+LleHNrCad/v3zNFK0PPCjIeBSWlI/1bPhJDCpfwpvJLk86DrB97Q3IafU2ml7DkC
-AQEEBAMCBPAwDQYJKoZIhvcNAQEEBQADgYEAWi2kXbK2N2DtFmKIpya31hC00PEl
+93bi3iaDy4jI1uskvlM516iaBQx1DCIa4gesluBAnZFvby8HX9y/A7tn5Ew2vdQJ
-CF+zVDQUGz5OtxelblRCoJkLQa3c6VkPwRnbVLqoyAlEQzdi+Vl/aub/20x8aNXo
+upkcCUswsU4MSA==
-DS9YqWSmV4JF0UGpOMspcBPrrB8aRra0u6DtDRJ8PhAho2Id4uycOqDKztLtSmEU
-GcK1vbZNKc09jRg=
 -----END CERTIFICATE-----
 -----END CERTIFICATE-----

+ 13 - 25
ndg/httpsclient/test/pki/localhost.key

@@ -1,27 +1,15 @@
 -----BEGIN RSA PRIVATE KEY-----
 -----BEGIN RSA PRIVATE KEY-----
-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
+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
-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
+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
-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
+0ubalTOW5/2yVab+uZ/vw4yxR64+KdyBuVopXV9STuh12Q0JSrXzdH82iQIDAQAB
-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
+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
-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
+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
-GWeYEfWVynYLx0M1PFMjtWewtSZZ0cU+StFw3QIDAQABAoIBABL7h2iPfO4EaKp5
+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
-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
+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
-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
+Nt1cBoOtAkEAxj1TuJRmZMf1QFuvv6DLloMmhilGkFobWysUZW18J8FyM+vI5kvH
-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
+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
-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
+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
-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
+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
-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
+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
-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
+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
-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
 -----END RSA PRIVATE KEY-----
 -----END RSA PRIVATE KEY-----

+ 40 - 0
ndg/httpsclient/test/test_https.py

@@ -19,6 +19,7 @@ from OpenSSL import SSL
 
 
 from ndg.httpsclient.test import Constants
 from ndg.httpsclient.test import Constants
 from ndg.httpsclient.https import HTTPSConnection
 from ndg.httpsclient.https import HTTPSConnection
+from ndg.httpsclient.ssl_peer_verification import ServerSSLCertVerification
 
 
 
 
 class TestHTTPSConnection(unittest.TestCase):
 class TestHTTPSConnection(unittest.TestCase):
@@ -74,6 +75,45 @@ class TestHTTPSConnection(unittest.TestCase):
         resp = conn.getresponse()
         resp = conn.getresponse()
         print('Response = %s' % resp.read())
         print('Response = %s' % resp.read())
 
 
+    def test04_ssl_verification_with_subj_alt_name(self):
+        ctx = SSL.Context(SSL.SSLv3_METHOD)
+        
+        verify_callback = ServerSSLCertVerification(hostname='localhost')
+            
+        ctx.set_verify(SSL.VERIFY_PEER, verify_callback)
+        ctx.set_verify_depth(9)
+        
+        # Set correct location for CA certs to verify with
+        ctx.load_verify_locations(None, Constants.CACERT_DIR)
+        
+        conn = HTTPSConnection(Constants.HOSTNAME, port=Constants.PORT,
+                               ssl_context=ctx)
+        conn.connect()
+        conn.request('GET', '/')
+        resp = conn.getresponse()
+        print('Response = %s' % resp.read())
+
+    def test04_ssl_verification_with_subj_common_name(self):
+        ctx = SSL.Context(SSL.SSLv3_METHOD)
+        
+        # Explicitly set verification of peer hostname using peer certificate
+        # subject common name
+        verify_callback = ServerSSLCertVerification(hostname='localhost',
+                                                    subj_alt_name_match=False)
+
+        ctx.set_verify(SSL.VERIFY_PEER, verify_callback)
+        ctx.set_verify_depth(9)
+        
+        # Set correct location for CA certs to verify with
+        ctx.load_verify_locations(None, Constants.CACERT_DIR)
+        
+        conn = HTTPSConnection(Constants.HOSTNAME, port=Constants.PORT,
+                               ssl_context=ctx)
+        conn.connect()
+        conn.request('GET', '/')
+        resp = conn.getresponse()
+        print('Response = %s' % resp.read())
+
         
         
 if __name__ == "__main__":
 if __name__ == "__main__":
     unittest.main()
     unittest.main()

+ 2 - 1
ndg/httpsclient/utils.py

@@ -110,7 +110,8 @@ def open_url(url, config):
     except Exception, exc:
     except Exception, exc:
         return_message = "Error: %s" % exc.__str__()
         return_message = "Error: %s" % exc.__str__()
         if config.debug:
         if config.debug:
-            print exc.__class__, exc.__str__()
+            import traceback
+            print traceback.format_exc()
     return (return_code, return_message, response)
     return (return_code, return_message, response)
 
 
 
 

+ 4 - 2
setup.py

@@ -14,11 +14,12 @@ SSL peer.
 Prerequisites
 Prerequisites
 =============
 =============
 This has been developed and tested for Python 2.6 and 2.7 with pyOpenSSL 0.13.  
 This has been developed and tested for Python 2.6 and 2.7 with pyOpenSSL 0.13.  
-Note that proxy support is only available from Python 2.6.2 onwards.
+Note that proxy support is only available from Python 2.6.2 onwards.  pyasn1 is 
+required for correct SSL verification with subjectAltNames.
 
 
 Installation
 Installation
 ============
 ============
-Installation can be performed using easy_install or pip.
+Installation can be performed using easy_install or pip.  
 
 
 Running ndg_httpclient
 Running ndg_httpclient
 ======================
 ======================
@@ -64,6 +65,7 @@ setup(
             ],
             ],
     },
     },
     install_requires = ['PyOpenSSL'],
     install_requires = ['PyOpenSSL'],
+    extras_require = {'subjectAltName_support': 'pyasn1'},
     classifiers = [
     classifiers = [
         'Development Status :: 3 - Alpha',
         'Development Status :: 3 - Alpha',
         'Environment :: Console',
         'Environment :: Console',