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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg_security_common/ndg/security/common/utils/m2crypto.py@6052
Revision 6052, 22.0 KB checked in by pjkersha, 10 years ago (diff)

Updated MyProxy? Cert extension app for use with improved SAML Attribute Query interface class AttributeQuerySslSOAPBinding

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__ = OPTNAMES
261    __slots__ += tuple(["_SSLContextProxy__%s" % name for name in __slots__])
262    del name
263   
264    VALID_DNS_PAT = re.compile(',\s*')
265   
266    def __init__(self):
267        self.__sslCertFilePath = None
268        self.__sslPriKeyFilePath = None
269        self.__sslPriKeyPwd = None
270        self.__sslCACertFilePath = None
271        self.__sslCACertDir = None
272        self.__sslValidDNs = []
273
274    def createCtx(self, depth=9, **kw):
275        """Create an M2Crypto SSL Context from this objects properties
276        @type depth: int
277        @param depth: max. depth of certificate to verify against
278        @type kw: dict
279        @param kw: M2Crypto.SSL.Context keyword arguments
280        @rtype: M2Crypto.SSL.Context
281        @return M2Crypto SSL context object
282        """
283        ctx = SSL.Context(**kw)
284       
285        # Configure context according to this proxy's attributes
286        if self.sslCertFilePath and self.sslPriKeyFilePath:
287            # Pass client certificate
288            ctx.load_cert(self.sslCertFilePath, 
289                          self.__sslPriKeyFilePath, 
290                          lambda *arg, **kw: self.sslPriKeyPwd)
291            log.debug("Set client certificate and key in SSL Context")
292        else:
293            log.debug("No client certificate or key set in SSL Context")
294           
295        if self.sslCACertFilePath or self.sslCACertDir:
296            # Set CA certificates in order to verify peer
297            ctx.load_verify_locations(self.sslCACertFilePath, 
298                                      self.sslCACertDir)
299            mode = SSL.verify_peer|SSL.verify_fail_if_no_peer_cert
300        else:
301            mode = SSL.verify_fail_if_no_peer_cert
302            log.warning('No CA certificate files set: mode set to '
303                        '"verify_fail_if_no_peer_cert" only')
304           
305        if len(self.sslValidDNs) > 0:
306            # Set custom callback in order to verify peer certificate DN
307            # against whitelist
308            callback = self.createVerifySSLPeerCertCallback()
309            log.debug('Set peer certificate Distinguished Name check set in '
310                      'SSL Context')
311        else:
312            callback = None
313            log.warning('No peer certificate Distinguished Name check set in '
314                        'SSL Context')
315           
316        ctx.set_verify(mode, depth, callback=callback)
317           
318        return ctx
319 
320    def copy(self, sslCtxProxy):
321        """Copy settings from another context object
322        """
323        if not isinstance(sslCtxProxy, SSLContextProxy):
324            raise TypeError('Expecting %r for copy method input object; '
325                            'got %r' % (SSLContextProxy, type(sslCtxProxy)))
326       
327        for name in SSLContextProxy.OPTNAMES:
328            setattr(self, name, getattr(sslCtxProxy, name))
329           
330    def createVerifySSLPeerCertCallback(self):
331        """Create a callback function to enable the DN of the peer in an SSL
332        connection to be verified against a whitelist. 
333       
334        Nb. Making this function within the scope of a method of the class to
335        enables to access instance variables
336        """
337       
338        def _verifySSLPeerCertCallback(preVerifyOK, x509StoreCtx):
339            '''SSL verify callback function used to control the behaviour when
340            the SSL_VERIFY_PEER flag is set.  See:
341           
342            http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html
343           
344            This implementation applies verification in order to check the DN
345            of the peer certificate against a whitelist
346           
347            @type preVerifyOK: int
348            @param preVerifyOK: If a verification error is found, this
349            parameter will be set to 0
350            @type x509StoreCtx: M2Crypto.X509.X509_Store_Context
351            @param x509StoreCtx: locate the certificate to be verified and
352            perform additional verification steps as needed
353            @rtype: int
354            @return: controls the strategy of the further verification process.
355            - If verify_callback returns 0, the verification process is
356            immediately stopped with "verification failed" state. If
357            SSL_VERIFY_PEER is set, a verification failure alert is sent to the
358            peer and the TLS/SSL handshake is terminated.
359            - If verify_callback returns 1, the verification process is
360            continued.
361            If verify_callback always returns 1, the TLS/SSL handshake will not
362            be terminated with respect to verification failures and the
363            connection
364            will be established. The calling process can however retrieve the
365            error code of the last verification error using
366            SSL_get_verify_result or by maintaining its own error storage
367            managed by verify_callback.
368            '''
369            if preVerifyOK == 0:
370                # Something is wrong with the certificate don't bother
371                # proceeding any further
372                log.error("verifyCallback: pre-verify OK flagged an error "
373                          "with the peer certificate, returning error state "
374                          "to caller ...")
375                return preVerifyOK
376           
377            x509CertChain = x509StoreCtx.get1_chain()
378            for cert in x509CertChain:
379                x509Cert = X509Cert.fromM2Crypto(cert)
380                if x509Cert.dn in self.sslValidDNs:
381                    return preVerifyOK
382               
383                subject = cert.get_subject()
384                dn = subject.as_text()
385                log.debug("verifyCallback: dn = %r", dn)
386               
387            # No match found so return fail status
388            return SSLContextProxy.PRE_VERIFY_FAIL
389       
390        return _verifySSLPeerCertCallback
391
392    def _getSSLCertFilePath(self):
393        return self.__sslCertFilePath
394   
395    def _setSSLCertFilePath(self, filePath):
396        "Set X.509 cert file path property method"
397       
398        if isinstance(filePath, basestring):
399            filePath = os.path.expandvars(filePath)
400           
401        elif filePath is not None:
402            raise TypeError("X.509 cert. file path must be a valid string")
403       
404        self.__sslCertFilePath = filePath
405               
406    sslCertFilePath = property(fset=_setSSLCertFilePath,
407                               fget=_getSSLCertFilePath,
408                               doc="File path to X.509 cert.")
409       
410    def _getSSLCACertFilePath(self):
411        """Get file path for list of CA cert or certs used to validate SSL
412        connections
413       
414        @rtype sslCACertFilePath: basestring
415        @return sslCACertFilePathList: file path to file containing concatenated
416        PEM encoded CA certificates."""
417        return self.__sslCACertFilePath
418   
419    def _setSSLCACertFilePath(self, value):
420        """Set CA cert file path
421       
422        @type sslCACertFilePath: basestring, list, tuple or None
423        @param sslCACertFilePath: file path to CA certificate file.  If None
424        then the input is quietly ignored."""
425        if isinstance(value, basestring):
426            self.__sslCACertFilePath = os.path.expandvars(value)
427           
428        elif value is None:
429            self.__sslCACertFilePath = value
430           
431        else:
432            raise TypeError("Input CA Certificate file path must be "
433                            "a valid string or None type: %r" % type(value)) 
434       
435       
436    sslCACertFilePath = property(fget=_getSSLCACertFilePath,
437                                 fset=_setSSLCACertFilePath,
438                                 doc="Path to file containing concatenated PEM "
439                                     "encoded CA Certificates - used for "
440                                     "verification of peer certs in SSL "
441                                     "connection")
442       
443    def _getSSLCACertDir(self):
444        """Get file path for list of CA cert or certs used to validate SSL
445        connections
446       
447        @rtype sslCACertDir: basestring
448        @return sslCACertDirList: directory containing PEM encoded CA
449        certificates."""
450        return self.__sslCACertDir
451   
452    def _setSSLCACertDir(self, value):
453        """Set CA cert or certs to validate AC signatures, signatures
454        of Attribute Authority SOAP responses and SSL connections where
455        AA SOAP service is run over SSL.
456       
457        @type sslCACertDir: basestring
458        @param sslCACertDir: directory containing CA certificate files.
459        """
460        if isinstance(value, basestring):
461            self.__sslCACertDir = os.path.expandvars(value)
462        elif value is None:
463            self.__sslCACertDir = value
464        else:
465            raise TypeError("Input CA Certificate directroy must be "
466                            "a valid string or None type: %r" % type(value))     
467       
468    sslCACertDir = property(fget=_getSSLCACertDir,
469                            fset=_setSSLCACertDir,
470                            doc="Path to directory containing PEM encoded CA "
471                                "Certificates used for verification of peer "
472                                "certs in SSL connection.   Files in the "
473                                "directory must be named with the form "
474                                "<hash>.0 where <hash> can be obtained using "
475                                "openssl x509 -in cert -hash -noout or using "
476                                "the c_rehash OpenSSL script")
477   
478    def _getSslValidDNs(self):
479        return self.__sslValidDNs
480
481    def _setSslValidDNs(self, value):
482        if isinstance(value, basestring): 
483            pat = SSLContextProxy.VALID_DNS_PAT
484            self.__sslValidDNs = [X500DN.fromString(dn) 
485                                  for dn in pat.split(value)]
486           
487        elif isinstance(value, (tuple, list)):
488            self.__sslValidDNs = [X500DN.fromString(dn) for dn in value]
489        else:
490            raise TypeError('Expecting list/tuple or basestring type for "%s" '
491                            'attribute; got %r' %
492                            (SSLContextProxy.SSL_VALID_DNS_OPTNAME, 
493                             type(value)))
494   
495    sslValidDNs = property(_getSslValidDNs, 
496                           _setSslValidDNs, 
497                           doc="whitelist of acceptable certificate "
498                               "Distinguished Names for peer certificates in "
499                               "SSL requests")
500
501    def _getSSLPriKeyFilePath(self):
502        return self.__sslPriKeyFilePath
503   
504    def _setSSLPriKeyFilePath(self, filePath):
505        "Set ssl private key file path property method"
506       
507        if isinstance(filePath, basestring):
508            filePath = os.path.expandvars(filePath)
509
510        elif filePath is not None:
511            raise TypeError("Private key file path must be a valid "
512                            "string or None type")
513       
514        self.__sslPriKeyFilePath = filePath
515       
516    sslPriKeyFilePath = property(fget=_getSSLPriKeyFilePath,
517                                 fset=_setSSLPriKeyFilePath,
518                                 doc="File path to SSL private key")
519 
520    def _setSSLPriKeyPwd(self, sslPriKeyPwd):
521        "Set method for ssl private key file password"
522        if not isinstance(sslPriKeyPwd, (type(None), basestring)):
523            raise TypeError("Signing private key password must be None "
524                            "or a valid string")
525       
526        # Explicitly convert to string as M2Crypto OpenSSL wrapper fails with
527        # unicode type
528        self.__sslPriKeyPwd = str(sslPriKeyPwd)
529
530    def _getSSLPriKeyPwd(self):
531        "Get property method for SSL private key"
532        return self.__sslPriKeyPwd
533       
534    sslPriKeyPwd = property(fset=_setSSLPriKeyPwd,
535                             fget=_getSSLPriKeyPwd,
536                             doc="Password protecting SSL private key file")
537
538    def __getstate__(self):
539        '''Enable pickling for use with beaker.session'''
540        return dict([(attrName, getattr(self, attrName))
541                     for attrName in self.__class__.__slots__])
542       
543    def __setstate__(self, attrDict):
544        '''Enable pickling for use with beaker.session'''
545        for attr, val in attrDict.items():
546            setattr(self, attr, val)
Note: See TracBrowser for help on using the repository browser.