source: TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/utils/m2crypto.py @ 7076

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/NDGSecurity/python/ndg_security_common/ndg/security/common/utils/m2crypto.py@7076
Revision 7076, 22.2 KB checked in by pjkersha, 10 years ago (diff)
  • 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$'
11import logging
12log = logging.getLogger(__name__)
13
14import os
15import re
16
17from M2Crypto import SSL, X509
18from M2Crypto.httpslib import HTTPSConnection as _HTTPSConnection
19
20from ndg.security.common.X509 import X509Cert, X509Stack, X500DN
21
22
23class InvalidCertSignature(SSL.Checker.SSLVerificationError):
24    """Raise if verification against CA cert public key fails"""
25
26
27class InvalidCertDN(SSL.Checker.SSLVerificationError):
28    """Raise if verification against a list acceptable DNs fails"""
29   
30
31class HostCheck(SSL.Checker.Checker, object):
32    """Override SSL.Checker.Checker to enable alternate Common Name
33    setting match for peer cert"""
34
35    def __init__(self, 
36                 peerCertDN=None, 
37                 peerCertCN=None,
38                 acceptedDNs=[], 
39                 caCertList=[],
40                 caCertFilePathList=[], 
41                 **kw):
42        """Override parent class __init__ to enable setting of myProxyServerDN
43        setting
44       
45        @type peerCertDN: string/list
46        @param peerCertDN: Set the expected Distinguished Name of the
47        server to avoid errors matching hostnames.  This is useful
48        where the hostname is not fully qualified. 
49
50        *param acceptedDNs: a list of acceptable DNs.  This enables validation
51        where the expected DN is where against a limited list of certs.
52       
53        @type peerCertCN: string
54        @param peerCertCN: enable alternate Common Name to peer
55        hostname
56       
57        @type caCertList: list type of M2Crypto.X509.X509 types
58        @param caCertList: CA X.509 certificates - if set the peer cert's
59        CA signature is verified against one of these.  At least one must
60        verify
61       
62        @type caCertFilePathList: list string types
63        @param caCertFilePathList: same as caCertList except input as list
64        of CA cert file paths"""
65       
66        SSL.Checker.Checker.__init__(self, **kw)
67       
68        self.peerCertDN = peerCertDN
69        self.peerCertCN = peerCertCN
70        self.acceptedDNs = acceptedDNs
71       
72        if caCertList:
73            self.caCertList = caCertList
74        elif caCertFilePathList:
75            self.caCertFilePathList = caCertFilePathList
76        else:
77            # Set default to enable len() test in __call__
78            self.__caCertStack = ()
79           
80    def __call__(self, peerCert, host=None):
81        """Carry out checks on server ID
82        @param peerCert: MyProxy server host certificate as M2Crypto.X509.X509
83        instance
84        @param host: name of host to check
85        """
86        if peerCert is None:
87            raise SSL.Checker.NoCertificate('SSL Peer did not return '
88                                            'certificate')
89
90        peerCertDN = '/'+peerCert.get_subject().as_text().replace(', ', '/')
91        try:
92            SSL.Checker.Checker.__call__(self, peerCert, host=self.peerCertCN)
93           
94        except SSL.Checker.WrongHost, e:
95            # Try match against peerCertDN set   
96            if peerCertDN != self.peerCertDN:
97                raise e
98
99        # At least one match should be found in the list - first convert to
100        # NDG X500DN type to allow per field matching for DN comparison
101        peerCertX500DN = X500DN(dn=peerCertDN)
102       
103        if self.acceptedDNs:
104           matchFound = False
105           for dn in self.acceptedDNs:
106               x500dn = X500DN(dn=dn)
107               if x500dn == peerCertX500DN:
108                   matchFound = True
109                   break
110               
111           if not matchFound:
112               raise InvalidCertDN('Peer cert DN "%s" doesn\'t match '
113                                   'verification list' % peerCertDN)
114
115        if len(self.__caCertStack) > 0:
116            try:
117                self.__caCertStack.verifyCertChain(
118                           x509Cert2Verify=X509Cert(m2CryptoX509=peerCert))
119            except Exception, e:
120                raise InvalidCertSignature("Peer certificate verification "
121                                           "against CA certificate failed: %s" 
122                                           % e)
123             
124        # They match - drop the exception and return all OK instead         
125        return True
126     
127    def __setCACertList(self, caCertList):
128        """Set list of CA certs - peer cert must validate against at least one
129        of these"""
130        self.__caCertStack = X509Stack()
131        for caCert in caCertList:
132            self.__caCertStack.push(caCert)
133
134    caCertList = property(fset=__setCACertList,
135                          doc="list of CA certificates - the peer certificate "
136                              "must validate against one")
137
138    def __setCACertsFromFileList(self, caCertFilePathList):
139        '''Read CA certificates from file and add them to the X.509
140        stack
141       
142        @type caCertFilePathList: basestring, list or tuple
143        @param caCertFilePathList: list of file paths for CA certificates to
144        be used to verify certificate used to sign message.  If a single
145        string item is input then this is converted into a tuple
146        '''
147        if isinstance(caCertFilePathList, basestring):
148            caCertFilePathList = (caCertFilePathList,)
149           
150        elif not isinstance(caCertFilePathList, (list, tuple)):
151            raise TypeError('Expecting a basestring, list or tuple type for '
152                            '"caCertFilePathList"')
153
154        self.__caCertStack = X509Stack()
155
156        for caCertFilePath in caCertFilePathList:
157            self.__caCertStack.push(X509.load_cert(caCertFilePath))
158       
159    caCertFilePathList = property(fset=__setCACertsFromFileList,
160                                  doc="list of CA certificate file paths - "
161                                      "peer certificate must validate against "
162                                      "one")
163
164
165class HTTPSConnection(_HTTPSConnection):
166    """Modified version of M2Crypto equivalent to enable custom checks with
167    the peer and timeout settings
168   
169    @type defReadTimeout: M2Crypto.SSL.timeout
170    @cvar defReadTimeout: default timeout for read operations
171    @type defWriteTimeout: M2Crypto.SSL.timeout
172    @cvar defWriteTimeout: default timeout for write operations"""   
173    defReadTimeout = SSL.timeout(sec=20.)
174    defWriteTimeout = SSL.timeout(sec=20.)
175   
176    def __init__(self, *args, **kw):
177        '''Overload to enable setting of post connection check
178        callback to SSL.Connection
179       
180        type *args: tuple
181        param *args: args which apply to M2Crypto.httpslib.HTTPSConnection
182        type **kw: dict
183        param **kw: additional keywords
184        @type postConnectionCheck: SSL.Checker.Checker derivative
185        @keyword postConnectionCheck: set class for checking peer
186        @type readTimeout: M2Crypto.SSL.timeout
187        @keyword readTimeout: readTimeout - set timeout for read
188        @type writeTimeout: M2Crypto.SSL.timeout
189        @keyword writeTimeout: similar to read timeout'''
190       
191        self._postConnectionCheck = kw.pop('postConnectionCheck',
192                                           SSL.Checker.Checker)
193       
194        if 'readTimeout' in kw:
195            if not isinstance(kw['readTimeout'], SSL.timeout):
196                raise AttributeError("readTimeout must be of type "
197                                     "M2Crypto.SSL.timeout")
198            self.readTimeout = kw.pop('readTimeout')
199        else:
200            self.readTimeout = HTTPSConnection.defReadTimeout
201             
202        if 'writeTimeout' in kw:
203            if not isinstance(kw['writeTimeout'], SSL.timeout):
204                raise AttributeError("writeTimeout must be of type "
205                                     "M2Crypto.SSL.timeout") 
206            self.writeTimeout = kw.pop('writeTimeout')
207        else:
208            self.writeTimeout = HTTPSConnection.defWriteTimeout
209   
210        self._clntCertFilePath = kw.pop('clntCertFilePath', None)
211        self._clntPriKeyFilePath = kw.pop('clntPriKeyFilePath', None)
212       
213        _HTTPSConnection.__init__(self, *args, **kw)
214       
215        # load up certificate stuff
216        if (self._clntCertFilePath is not None and 
217            self._clntPriKeyFilePath is not None):
218            self.ssl_ctx.load_cert(self._clntCertFilePath, 
219                                   self._clntPriKeyFilePath)
220       
221       
222    def connect(self):
223        '''Overload M2Crypto.httpslib.HTTPSConnection to enable
224        custom post connection check of peer certificate and socket timeout'''
225
226        self.sock = SSL.Connection(self.ssl_ctx)
227        self.sock.set_post_connection_check_callback(self._postConnectionCheck)
228
229        self.sock.set_socket_read_timeout(self.readTimeout)
230        self.sock.set_socket_write_timeout(self.writeTimeout)
231
232        self.sock.connect((self.host, self.port))
233
234    def putrequest(self, method, url, **kw):
235        '''Overload to work around bug with unicode type URL'''
236        url = str(url)
237        _HTTPSConnection.putrequest(self, method, url, **kw) 
238         
239             
240class SSLContextProxy(object):
241    """Holder for M2Crypto.SSL.Context parameters"""
242    PRE_VERIFY_FAIL, PRE_VERIFY_OK = range(2)
243   
244    SSL_CERT_FILEPATH_OPTNAME = "sslCertFilePath"
245    SSL_PRIKEY_FILEPATH_OPTNAME = "sslPriKeyFilePath"
246    SSL_PRIKEY_PWD_OPTNAME = "sslPriKeyPwd"
247    SSL_CACERT_FILEPATH_OPTNAME = "sslCACertFilePath"
248    SSL_CACERT_DIRPATH_OPTNAME = "sslCACertDir"
249    SSL_VALID_DNS_OPTNAME = "sslValidDNs"
250   
251    OPTNAMES = (
252        SSL_CERT_FILEPATH_OPTNAME,
253        SSL_PRIKEY_FILEPATH_OPTNAME,
254        SSL_PRIKEY_PWD_OPTNAME,
255        SSL_CACERT_FILEPATH_OPTNAME,
256        SSL_CACERT_DIRPATH_OPTNAME,
257        SSL_VALID_DNS_OPTNAME
258    )
259   
260    __slots__ = tuple(["__%s" % name for name in OPTNAMES])
261    del name
262   
263    VALID_DNS_PAT = re.compile(',\s*')
264   
265    def __init__(self):
266        self.__sslCertFilePath = None
267        self.__sslPriKeyFilePath = None
268        self.__sslPriKeyPwd = None
269        self.__sslCACertFilePath = None
270        self.__sslCACertDir = None
271        self.__sslValidDNs = []
272
273    def createCtx(self, depth=9, **kw):
274        """Create an M2Crypto SSL Context from this objects properties
275        @type depth: int
276        @param depth: max. depth of certificate to verify against
277        @type kw: dict
278        @param kw: M2Crypto.SSL.Context keyword arguments
279        @rtype: M2Crypto.SSL.Context
280        @return M2Crypto SSL context object
281        """
282        ctx = SSL.Context(**kw)
283       
284        # Configure context according to this proxy's attributes
285        if self.sslCertFilePath and self.sslPriKeyFilePath:
286            # Pass client certificate
287            ctx.load_cert(self.sslCertFilePath, 
288                          self.__sslPriKeyFilePath, 
289                          lambda *arg, **kw: self.sslPriKeyPwd)
290            log.debug("Set client certificate and key in SSL Context")
291        else:
292            log.debug("No client certificate or key set in SSL Context")
293           
294        if self.sslCACertFilePath or self.sslCACertDir:
295            # Set CA certificates in order to verify peer
296            ctx.load_verify_locations(self.sslCACertFilePath, 
297                                      self.sslCACertDir)
298            mode = SSL.verify_peer|SSL.verify_fail_if_no_peer_cert
299        else:
300            mode = SSL.verify_fail_if_no_peer_cert
301            log.warning('No CA certificate files set: mode set to '
302                        '"verify_fail_if_no_peer_cert" only')
303           
304        if len(self.sslValidDNs) > 0:
305            # Set custom callback in order to verify peer certificate DN
306            # against whitelist
307            callback = self.createVerifySSLPeerCertCallback()
308            log.debug('Set peer certificate Distinguished Name check set in '
309                      'SSL Context')
310        else:
311            callback = None
312            log.warning('No peer certificate Distinguished Name check set in '
313                        'SSL Context')
314           
315        ctx.set_verify(mode, depth, callback=callback)
316           
317        return ctx
318 
319    def copy(self, sslCtxProxy):
320        """Copy settings from another context object
321        """
322        if not isinstance(sslCtxProxy, SSLContextProxy):
323            raise TypeError('Expecting %r for copy method input object; '
324                            'got %r' % (SSLContextProxy, type(sslCtxProxy)))
325       
326        for name in SSLContextProxy.OPTNAMES:
327            setattr(self, name, getattr(sslCtxProxy, name))
328           
329    def createVerifySSLPeerCertCallback(self):
330        """Create a callback function to enable the DN of the peer in an SSL
331        connection to be verified against a whitelist. 
332       
333        Nb. Making this function within the scope of a method of the class to
334        enables to access instance variables
335        """
336       
337        def _verifySSLPeerCertCallback(preVerifyOK, x509StoreCtx):
338            '''SSL verify callback function used to control the behaviour when
339            the SSL_VERIFY_PEER flag is set.  See:
340           
341            http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html
342           
343            This implementation applies verification in order to check the DN
344            of the peer certificate against a whitelist
345           
346            @type preVerifyOK: int
347            @param preVerifyOK: If a verification error is found, this
348            parameter will be set to 0
349            @type x509StoreCtx: M2Crypto.X509.X509_Store_Context
350            @param x509StoreCtx: locate the certificate to be verified and
351            perform additional verification steps as needed
352            @rtype: int
353            @return: controls the strategy of the further verification process.
354            - If verify_callback returns 0, the verification process is
355            immediately stopped with "verification failed" state. If
356            SSL_VERIFY_PEER is set, a verification failure alert is sent to the
357            peer and the TLS/SSL handshake is terminated.
358            - If verify_callback returns 1, the verification process is
359            continued.
360            If verify_callback always returns 1, the TLS/SSL handshake will not
361            be terminated with respect to verification failures and the
362            connection
363            will be established. The calling process can however retrieve the
364            error code of the last verification error using
365            SSL_get_verify_result or by maintaining its own error storage
366            managed by verify_callback.
367            '''
368            if preVerifyOK == 0:
369                # Something is wrong with the certificate don't bother
370                # proceeding any further
371                log.error("verifyCallback: pre-verify OK flagged an error "
372                          "with the peer certificate, returning error state "
373                          "to caller ...")
374                return preVerifyOK
375           
376            x509CertChain = x509StoreCtx.get1_chain()
377            for cert in x509CertChain:
378                x509Cert = X509Cert.fromM2Crypto(cert)
379                if x509Cert.dn in self.sslValidDNs:
380                    return preVerifyOK
381               
382                subject = cert.get_subject()
383                dn = subject.as_text()
384                log.debug("verifyCallback: dn = %r", dn)
385               
386            # No match found so return fail status
387            return SSLContextProxy.PRE_VERIFY_FAIL
388       
389        return _verifySSLPeerCertCallback
390
391    def _getSSLCertFilePath(self):
392        return self.__sslCertFilePath
393   
394    def _setSSLCertFilePath(self, filePath):
395        "Set X.509 cert file path property method"
396       
397        if isinstance(filePath, basestring):
398            filePath = os.path.expandvars(filePath)
399           
400        elif filePath is not None:
401            raise TypeError("X.509 cert. file path must be a valid string")
402       
403        self.__sslCertFilePath = filePath
404               
405    sslCertFilePath = property(fset=_setSSLCertFilePath,
406                               fget=_getSSLCertFilePath,
407                               doc="File path to X.509 cert.")
408       
409    def _getSSLCACertFilePath(self):
410        """Get file path for list of CA cert or certs used to validate SSL
411        connections
412       
413        @rtype sslCACertFilePath: basestring
414        @return sslCACertFilePathList: file path to file containing concatenated
415        PEM encoded CA certificates."""
416        return self.__sslCACertFilePath
417   
418    def _setSSLCACertFilePath(self, value):
419        """Set CA cert file path
420       
421        @type sslCACertFilePath: basestring, list, tuple or None
422        @param sslCACertFilePath: file path to CA certificate file.  If None
423        then the input is quietly ignored."""
424        if isinstance(value, basestring):
425            self.__sslCACertFilePath = os.path.expandvars(value)
426           
427        elif value is None:
428            self.__sslCACertFilePath = value
429           
430        else:
431            raise TypeError("Input CA Certificate file path must be "
432                            "a valid string or None type: %r" % type(value)) 
433       
434       
435    sslCACertFilePath = property(fget=_getSSLCACertFilePath,
436                                 fset=_setSSLCACertFilePath,
437                                 doc="Path to file containing concatenated PEM "
438                                     "encoded CA Certificates - used for "
439                                     "verification of peer certs in SSL "
440                                     "connection")
441       
442    def _getSSLCACertDir(self):
443        """Get file path for list of CA cert or certs used to validate SSL
444        connections
445       
446        @rtype sslCACertDir: basestring
447        @return sslCACertDirList: directory containing PEM encoded CA
448        certificates."""
449        return self.__sslCACertDir
450   
451    def _setSSLCACertDir(self, value):
452        """Set CA cert or certs to validate AC signatures, signatures
453        of Attribute Authority SOAP responses and SSL connections where
454        AA SOAP service is run over SSL.
455       
456        @type sslCACertDir: basestring
457        @param sslCACertDir: directory containing CA certificate files.
458        """
459        if isinstance(value, basestring):
460            self.__sslCACertDir = os.path.expandvars(value)
461        elif value is None:
462            self.__sslCACertDir = value
463        else:
464            raise TypeError("Input CA Certificate directroy must be "
465                            "a valid string or None type: %r" % type(value))     
466       
467    sslCACertDir = property(fget=_getSSLCACertDir,
468                            fset=_setSSLCACertDir,
469                            doc="Path to directory containing PEM encoded CA "
470                                "Certificates used for verification of peer "
471                                "certs in SSL connection.   Files in the "
472                                "directory must be named with the form "
473                                "<hash>.0 where <hash> can be obtained using "
474                                "openssl x509 -in cert -hash -noout or using "
475                                "the c_rehash OpenSSL script")
476   
477    def _getSslValidDNs(self):
478        return self.__sslValidDNs
479
480    def _setSslValidDNs(self, value):
481        if isinstance(value, basestring): 
482            pat = SSLContextProxy.VALID_DNS_PAT
483            self.__sslValidDNs = [X500DN.fromString(dn) 
484                                  for dn in pat.split(value)]
485           
486        elif isinstance(value, (tuple, list)):
487            self.__sslValidDNs = [X500DN.fromString(dn) for dn in value]
488        else:
489            raise TypeError('Expecting list/tuple or basestring type for "%s" '
490                            'attribute; got %r' %
491                            (SSLContextProxy.SSL_VALID_DNS_OPTNAME, 
492                             type(value)))
493   
494    sslValidDNs = property(_getSslValidDNs, 
495                           _setSslValidDNs, 
496                           doc="whitelist of acceptable certificate "
497                               "Distinguished Names for peer certificates in "
498                               "SSL requests")
499
500    def _getSSLPriKeyFilePath(self):
501        return self.__sslPriKeyFilePath
502   
503    def _setSSLPriKeyFilePath(self, filePath):
504        "Set ssl private key file path property method"
505       
506        if isinstance(filePath, basestring):
507            filePath = os.path.expandvars(filePath)
508
509        elif filePath is not None:
510            raise TypeError("Private key file path must be a valid "
511                            "string or None type")
512       
513        self.__sslPriKeyFilePath = filePath
514       
515    sslPriKeyFilePath = property(fget=_getSSLPriKeyFilePath,
516                                 fset=_setSSLPriKeyFilePath,
517                                 doc="File path to SSL private key")
518 
519    def _setSSLPriKeyPwd(self, sslPriKeyPwd):
520        "Set method for ssl private key file password"
521        if not isinstance(sslPriKeyPwd, (type(None), basestring)):
522            raise TypeError("Signing private key password must be None "
523                            "or a valid string")
524       
525        # Explicitly convert to string as M2Crypto OpenSSL wrapper fails with
526        # unicode type
527        self.__sslPriKeyPwd = str(sslPriKeyPwd)
528
529    def _getSSLPriKeyPwd(self):
530        "Get property method for SSL private key"
531        return self.__sslPriKeyPwd
532       
533    sslPriKeyPwd = property(fset=_setSSLPriKeyPwd,
534                             fget=_getSSLPriKeyPwd,
535                             doc="Password protecting SSL private key file")
536
537    def __getstate__(self):
538        '''Enable pickling for use with beaker.session'''
539        _dict = {}
540        for attrName in SSLContextProxy.__slots__:
541            # Ugly hack to allow for derived classes setting private member
542            # variables
543            if attrName.startswith('__'):
544                attrName = "_SSLContextProxy" + attrName
545               
546            _dict[attrName] = getattr(self, attrName)
547           
548        return _dict
549       
550    def __setstate__(self, attrDict):
551        '''Enable pickling for use with beaker.session'''
552        for attr, val in attrDict.items():
553            setattr(self, attr, val)
Note: See TracBrowser for help on using the repository browser.