source: TI12-security/trunk/python/ndg.security.common/ndg/security/common/m2CryptoSSLUtility.py @ 5442

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/m2CryptoSSLUtility.py@5442
Revision 5442, 9.2 KB checked in by pjkersha, 11 years ago (diff)

Bug fix: ndg.security.common.m2CryptoSSLUtility.HostCheck? can know accept a single string parameter for caCertFilePathList input

  • Property svn:keywords set to Id
Line 
1"""Extend M2Crypto SSL functionality for cert verification and custom
2timeout settings.
3
4NERC DataGrid Project"""
5__author__ = "P J Kershaw"
6__date__ = "02/07/07"
7__copyright__ = "(C) 2009 Science and Technology Facilities Council"
8__license__ = "BSD - see LICENSE file in top-level directory"
9__contact__ = "Philip.Kershaw@stfc.ac.uk"
10__revision__ = '$Id$'
11
12import httplib
13import socket
14
15from M2Crypto import SSL, X509
16from M2Crypto.httpslib import HTTPSConnection as _HTTPSConnection
17
18from ndg.security.common.X509 import X509Cert, X509Stack, X500DN
19
20class InvalidCertSignature(SSL.Checker.SSLVerificationError):
21    """Raise if verification against CA cert public key fails"""
22
23class InvalidCertDN(SSL.Checker.SSLVerificationError):
24    """Raise if verification against a list acceptable DNs fails"""
25   
26
27class HostCheck(SSL.Checker.Checker, object):
28    """Override SSL.Checker.Checker to enable alternate Common Name
29    setting match for peer cert"""
30
31    def __init__(self, 
32                 peerCertDN=None, 
33                 peerCertCN=None,
34                 acceptedDNs=[], 
35                 caCertList=[],
36                 caCertFilePathList=[], 
37                 **kw):
38        """Override parent class __init__ to enable setting of myProxyServerDN
39        setting
40       
41        @type peerCertDN: string/list
42        @param peerCertDN: Set the expected Distinguished Name of the
43        server to avoid errors matching hostnames.  This is useful
44        where the hostname is not fully qualified. 
45
46        *param acceptedDNs: a list of acceptable DNs.  This enables validation
47        where the expected DN is where against a limited list of certs.
48       
49        @type peerCertCN: string
50        @param peerCertCN: enable alternate Common Name to peer
51        hostname
52       
53        @type caCertList: list type of M2Crypto.X509.X509 types
54        @param caCertList: CA X.509 certificates - if set the peer cert's
55        CA signature is verified against one of these.  At least one must
56        verify
57       
58        @type caCertFilePathList: list string types
59        @param caCertFilePathList: same as caCertList except input as list
60        of CA cert file paths"""
61       
62        SSL.Checker.Checker.__init__(self, **kw)
63       
64        self.peerCertDN = peerCertDN
65        self.peerCertCN = peerCertCN
66        self.acceptedDNs = acceptedDNs
67       
68        if caCertList:
69            self.caCertList = caCertList
70        elif caCertFilePathList:
71            self.caCertFilePathList = caCertFilePathList
72        else:
73            # Set default to enable len() test in __call__
74            self.__caCertStack = ()
75           
76    def __call__(self, peerCert, host=None):
77        """Carry out checks on server ID
78        @param peerCert: MyProxy server host certificate as M2Crypto.X509.X509
79        instance
80        @param host: name of host to check
81        """
82        if peerCert is None:
83            raise SSL.Checker.NoCertificate('SSL Peer did not return '
84                                            'certificate')
85
86        peerCertDN = '/'+peerCert.get_subject().as_text().replace(', ', '/')
87        try:
88            SSL.Checker.Checker.__call__(self, peerCert, host=self.peerCertCN)
89           
90        except SSL.Checker.WrongHost, e:
91            # Try match against peerCertDN set   
92            if peerCertDN != self.peerCertDN:
93                raise e
94
95        # At least one match should be found in the list - first convert to
96        # NDG X500DN type to allow per field matching for DN comparison
97        peerCertX500DN = X500DN(dn=peerCertDN)
98       
99        if self.acceptedDNs:
100           matchFound = False
101           for dn in self.acceptedDNs:
102               x500dn = X500DN(dn=dn)
103               if x500dn == peerCertX500DN:
104                   matchFound = True
105                   break
106               
107           if not matchFound:
108               raise InvalidCertDN('Peer cert DN "%s" doesn\'t match '
109                                   'verification list' % peerCertDN)
110
111        if len(self.__caCertStack) > 0:
112            try:
113                self.__caCertStack.verifyCertChain(
114                           x509Cert2Verify=X509Cert(m2CryptoX509=peerCert))
115            except Exception, e:
116                raise InvalidCertSignature("Peer certificate verification "
117                                           "against CA certificate failed: %s" 
118                                           % e)
119             
120        # They match - drop the exception and return all OK instead         
121        return True
122     
123    def __setCACertList(self, caCertList):
124        """Set list of CA certs - peer cert must validate against at least one
125        of these"""
126        self.__caCertStack = X509Stack()
127        for caCert in caCertList:
128            self.__caCertStack.push(caCert)
129
130    caCertList = property(fset=__setCACertList,
131                          doc="list of CA certificates - the peer certificate "
132                              "must validate against one")
133
134    def __setCACertsFromFileList(self, caCertFilePathList):
135        '''Read CA certificates from file and add them to the X.509
136        stack
137       
138        @type caCertFilePathList: basestring, list or tuple
139        @param caCertFilePathList: list of file paths for CA certificates to
140        be used to verify certificate used to sign message.  If a single
141        string item is input then this is converted into a tuple
142        '''
143        if isinstance(caCertFilePathList, basestring):
144            caCertFilePathList = (caCertFilePathList,)
145           
146        elif not isinstance(caCertFilePathList, (list, tuple)):
147            raise TypeError('Expecting a basestring, list or tuple type for '
148                            '"caCertFilePathList"')
149
150        self.__caCertStack = X509Stack()
151
152        for caCertFilePath in caCertFilePathList:
153            self.__caCertStack.push(X509.load_cert(caCertFilePath))
154       
155    caCertFilePathList = property(fset=__setCACertsFromFileList,
156                                  doc="list of CA certificate file paths - "
157                                      "peer certificate must validate against "
158                                      "one")
159
160
161class HTTPSConnection(_HTTPSConnection):
162    """Modified version of M2Crypto equivalent to enable custom checks with
163    the peer and timeout settings
164   
165    @type defReadTimeout: M2Crypto.SSL.timeout
166    @cvar defReadTimeout: default timeout for read operations
167    @type defWriteTimeout: M2Crypto.SSL.timeout
168    @cvar defWriteTimeout: default timeout for write operations"""   
169    defReadTimeout = SSL.timeout(sec=20.)
170    defWriteTimeout = SSL.timeout(sec=20.)
171   
172    def __init__(self, *args, **kw):
173        '''Overload to enable setting of post connection check
174        callback to SSL.Connection
175       
176        type *args: tuple
177        param *args: args which apply to M2Crypto.httpslib.HTTPSConnection
178        type **kw: dict
179        param **kw: additional keywords
180        @type postConnectionCheck: SSL.Checker.Checker derivative
181        @keyword postConnectionCheck: set class for checking peer
182        @type readTimeout: M2Crypto.SSL.timeout
183        @keyword readTimeout: readTimeout - set timeout for read
184        @type writeTimeout: M2Crypto.SSL.timeout
185        @keyword writeTimeout: similar to read timeout'''
186       
187        self._postConnectionCheck = kw.pop('postConnectionCheck',
188                                           SSL.Checker.Checker)
189       
190        if 'readTimeout' in kw:
191            if not isinstance(kw['readTimeout'], SSL.timeout):
192                raise AttributeError("readTimeout must be of type "
193                                     "M2Crypto.SSL.timeout")
194            self.readTimeout = kw.pop('readTimeout')
195        else:
196            self.readTimeout = HTTPSConnection.defReadTimeout
197             
198        if 'writeTimeout' in kw:
199            if not isinstance(kw['writeTimeout'], SSL.timeout):
200                raise AttributeError("writeTimeout must be of type "
201                                     "M2Crypto.SSL.timeout") 
202            self.writeTimeout = kw.pop('writeTimeout')
203        else:
204            self.writeTimeout = HTTPSConnection.defWriteTimeout
205   
206        self._clntCertFilePath = kw.pop('clntCertFilePath', None)
207        self._clntPriKeyFilePath = kw.pop('clntPriKeyFilePath', None)
208       
209        _HTTPSConnection.__init__(self, *args, **kw)
210       
211        # load up certificate stuff
212        if self._clntCertFilePath is not None and \
213           self._clntPriKeyFilePath is not None:
214            self.ssl_ctx.load_cert(self._clntCertFilePath, 
215                                   self._clntPriKeyFilePath)
216       
217       
218    def connect(self):
219        '''Overload M2Crypto.httpslib.HTTPSConnection to enable
220        custom post connection check of peer certificate and socket timeout'''
221
222        self.sock = SSL.Connection(self.ssl_ctx)
223        self.sock.set_post_connection_check_callback(self._postConnectionCheck)
224
225        self.sock.set_socket_read_timeout(self.readTimeout)
226        self.sock.set_socket_write_timeout(self.writeTimeout)
227
228        self.sock.connect((self.host, self.port))
229
230    def putrequest(self, method, url, **kw):
231        '''Overload to work around bug with unicode type URL'''
232        url = str(url)
233        _HTTPSConnection.putrequest(self, method, url, **kw)
Note: See TracBrowser for help on using the repository browser.