Parcourir la 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 il y a 13 ans
Parent
commit
32902723fd

+ 63 - 2
ndg/httpsclient/ssl_peer_verification.py

@@ -10,6 +10,12 @@ import re
 import logging
 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):
     """Check server identity.  If hostname doesn't match, allow match of
@@ -26,12 +32,13 @@ class ServerSSLCertVerification(object):
         'domainComponent':          'DC',
         'userid':                   'UID'
     }
+    SUBJ_ALT_NAME_EXT_NAME = 'subjectAltName'
     PARSER_RE_STR = '/(%s)=' % '|'.join(DN_LUT.keys() + DN_LUT.values())
     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
         setting
 
@@ -39,6 +46,13 @@ class ServerSSLCertVerification(object):
         @param certDN: Set the expected Distinguished Name of the
         server to avoid errors matching hostnames.  This is useful
         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.__hostname = None
@@ -48,6 +62,18 @@ class ServerSSLCertVerification(object):
 
         if hostname is not None:
             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,
                  preverifyOK):
@@ -98,6 +124,13 @@ class ServerSSLCertVerification(object):
                               'certificate against')
                     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:
                     return preverifyOK
                 else:
@@ -115,6 +148,34 @@ class ServerSSLCertVerification(object):
         else:
             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):
         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-----
-MIICdzCCAeCgAwIBAgIBKDANBgkqhkiG9w0BAQQFADAzMQwwCgYDVQQKEwNOREcx
-ETAPBgNVBAsTCFNlY3VyaXR5MRAwDgYDVQQDEwdUZXN0IENBMB4XDTExMDcwNjEx
-MzIwNFoXDTEyMDcwNTExMzIwNFowNTEMMAoGA1UEChMDTkRHMREwDwYDVQQLEwhT
-ZWN1cml0eTESMBAGA1UEAxMJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEFAAOC
-AQ8AMIIBCgKCAQEAwOCUwunA35Y2uk0OP7xBUXtP/tiCR1L4Nlc1FTuDd7qEqqNI
-8AMLWrcxQI0/hwWeBsZyH8p97XM80HZNOzKJ5x9bhCe/i3IJKtRAjSzEwyNobPRi
-VajgLorItF+/4RjXaqYakG3mgxcQOpW12g1EP9/7bMWevx0EXxwWzp73nYH4/spD
-gl9ryHAX+OjzdNn7rBSvmY/9863ifo3zvYkV8DnqUXwpgYy8umNraeDBRi8nk4OF
-vou7qrh2w+yK9lDhw5CeRx0ZaEBiWROL6+iJZCDso7fnso+Y8mS0qm7Q8XP86u0Z
-GWeYEfWVynYLx0M1PFMjtWewtSZZ0cU+StFw3QIDAQABoxUwEzARBglghkgBhvhC
-AQEEBAMCBPAwDQYJKoZIhvcNAQEEBQADgYEAWi2kXbK2N2DtFmKIpya31hC00PEl
-CF+zVDQUGz5OtxelblRCoJkLQa3c6VkPwRnbVLqoyAlEQzdi+Vl/aub/20x8aNXo
-DS9YqWSmV4JF0UGpOMspcBPrrB8aRra0u6DtDRJ8PhAho2Id4uycOqDKztLtSmEU
-GcK1vbZNKc09jRg=
+MIICFjCCAX+gAwIBAgIBCjANBgkqhkiG9w0BAQQFADAzMQwwCgYDVQQKEwNOREcx
+ETAPBgNVBAsTCFNlY3VyaXR5MRAwDgYDVQQDEwdUZXN0IENBMB4XDTEyMDIwODE2
+MTE1M1oXDTE3MDIwNjE2MTE1M1owNTERMA8GA1UECxMIU2VjdXJpdHkxEjAQBgNV
+BAMTCWxvY2FsaG9zdDEMMAoGA1UEChMDTkRHMIGfMA0GCSqGSIb3DQEBAQUAA4GN
+ADCBiQKBgQCdhZgzD0xusZqzdphETJPgb4QK/sdDpF8EOT/20bAuyRgGt7papJmc
+6UtdgS5b9bGh6sRXx+vSKiTqq1ZFLOjnn3OQKhdrK2VU8XiD5rjuwTuNzser0uba
+lTOW5/2yVab+uZ/vw4yxR64+KdyBuVopXV9STuh12Q0JSrXzdH82iQIDAQABozgw
+NjAMBgNVHRMBAf8EAjAAMCYGA1UdEQQfMB2CCWxvY2FsaG9zdIIQbG9jYWxob3N0
+LmRvbWFpbjANBgkqhkiG9w0BAQQFAAOBgQBAAQCTkLfgYAjvm63KRXcE8djkYIVQ
+LleHNrCad/v3zNFK0PPCjIeBSWlI/1bPhJDCpfwpvJLk86DrB97Q3IafU2ml7DkC
+93bi3iaDy4jI1uskvlM516iaBQx1DCIa4gesluBAnZFvby8HX9y/A7tn5Ew2vdQJ
+upkcCUswsU4MSA==
 -----END CERTIFICATE-----

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

@@ -1,27 +1,15 @@
 -----BEGIN RSA PRIVATE KEY-----
-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
+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
 -----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.https import HTTPSConnection
+from ndg.httpsclient.ssl_peer_verification import ServerSSLCertVerification
 
 
 class TestHTTPSConnection(unittest.TestCase):
@@ -74,6 +75,45 @@ class TestHTTPSConnection(unittest.TestCase):
         resp = conn.getresponse()
         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__":
     unittest.main()

+ 2 - 1
ndg/httpsclient/utils.py

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

+ 4 - 2
setup.py

@@ -14,11 +14,12 @@ SSL peer.
 Prerequisites
 =============
 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 can be performed using easy_install or pip.
+Installation can be performed using easy_install or pip.  
 
 Running ndg_httpclient
 ======================
@@ -64,6 +65,7 @@ setup(
             ],
     },
     install_requires = ['PyOpenSSL'],
+    extras_require = {'subjectAltName_support': 'pyasn1'},
     classifiers = [
         'Development Status :: 3 - Alpha',
         'Environment :: Console',