source: TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/ssl.py @ 5751

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg_security_server/ndg/security/server/wsgi/ssl.py@5751
Revision 5751, 12.7 KB checked in by pjkersha, 10 years ago (diff)

Refactored properties and attributes.

Line 
1"""SSL Peer Authentication Middleware Module
2
3Apply to SSL client authentication to configured URL paths.
4
5SSL Client certificate is expected to be present in environ as SSL_CLIENT_CERT
6key as set by standard Apache SSL.
7
8NERC DataGrid Project
9"""
10__author__ = "P J Kershaw"
11__date__ = "11/12/08"
12__copyright__ = "(C) 2009 Science and Technology Facilities Council"
13__contact__ = "Philip.Kershaw@stfc.ac.uk"
14__revision__ = "$Id$"
15__license__ = "BSD - see top-level directory for LICENSE file"
16import logging
17log = logging.getLogger(__name__)
18import os
19import re # Pattern matching to determine which URI paths to apply SSL AuthN to
20
21from ndg.security.server.wsgi import NDGSecurityMiddlewareBase
22from ndg.security.common.X509 import X509Stack, X509Cert, X509CertError, X500DN
23from ndg.security.common.utils.classfactory import instantiateClass
24
25
26class ClientCertVerificationInterface(object):
27    """Interface to enable customised verification of the client certificate
28    Distinguished Name"""
29    def __init__(self, **cfg):
30        """@type cfg: dict
31        @param cfg: configuration parameters, derived class may customise
32        """
33        raise NotImplementedError()
34   
35    def __call__(self, x509Cert):
36        """Derived class implementation should return True if the certificate
37        DN is valid, False otherwise
38        @type x509Cert: ndg.security.common.X509.X509Cert
39        @param x509Cert: client X.509 certificate received from Apache
40        environment"""
41        raise NotImplementedError()
42
43
44class NoClientCertVerification(ClientCertVerificationInterface):
45    """Implementation of ClientCertVerificationInterface ignoring the
46    client certificate DN set"""
47    def __init__(self, **cfg):
48        pass
49   
50    def __call__(self, x509Cert):
51        return True
52   
53class ClientCertVerificationList(ClientCertVerificationInterface):
54    """Implementation of ClientCertVerificationInterface matching the input
55    client certificate DN against a configurable list"""
56   
57    def __init__(self, validDNList=[]):
58        self.validDNList = validDNList
59   
60    def __call__(self, x509Cert):
61        inputDN = x509Cert.dn
62        return inputDN in self._validDNList
63   
64    def _setValidDNList(self, dnList):
65        '''Read CA certificates from file and add them to an X.509 Cert.
66        stack
67       
68        @type dnList: list or tuple
69        @param dnList: list of DNs to match against the input certificate DN
70        '''
71       
72        if isinstance(dnList, basestring):
73            # Try parsing a space separated list of file paths
74            dnList = dnList.split()
75           
76        elif not isinstance(dnList, (list, tuple)):
77            raise TypeError('Expecting a list or tuple for "dnList"')
78
79        self._validDNList = [X500DN(dn) for dn in dnList]
80
81   
82    def _getValidDNList(self):
83        return self._validDNList
84   
85    validDNList = property(fset=_setValidDNList,
86                           fget=_getValidDNList,
87                           doc="list of permissible certificate Distinguished "
88                               "Names permissible")
89   
90
91class ApacheSSLAuthNMiddleware(NDGSecurityMiddlewareBase):
92    """Perform SSL peer certificate authentication making use of Apache
93    SSL environment settings
94   
95    B{This class relies on SSL environment settings being present as available
96    when run embedded within Apache using for example mod_wsgi}
97   
98    - SSL Client certificate is expected to be present in environ as
99    SSL_CLIENT_CERT key as set by Apache SSL with ExportCertData option to
100    SSLOptions directive enabled.
101    """
102    SSL_KEYNAME = 'HTTPS'
103    SSL_KEYVALUE = '1'
104   
105    _isSSLRequest = lambda self: self.environ.get(
106                                    ApacheSSLAuthNMiddleware.SSL_KEYNAME) == \
107                                    ApacheSSLAuthNMiddleware.SSL_KEYVALUE
108    isSSLRequest = property(fget=_isSSLRequest,
109                            doc="Is an SSL request boolean - depends on "
110                                "'HTTPS' Apache environment variable setting")
111   
112    SSL_CLIENT_CERT_KEYNAME = 'SSL_CLIENT_CERT'
113   
114    propertyDefaults = {
115        'clientCertVerificationClassName': None,
116        'rePathMatchList': [],
117        'caCertFilePathList': []
118    }
119    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
120       
121    _isSSLClientCertSet = lambda self: bool(self.environ.get(
122                            ApacheSSLAuthNMiddleware.SSL_CLIENT_CERT_KEYNAME)) 
123    isSSLClientCertSet = property(fget=_isSSLClientCertSet,
124                                  doc="Check for client X.509 certificate "
125                                      "'SSL_CLIENT_CERT' setting in environ")
126   
127    def __init__(self, app, global_conf, prefix='sslAuthn.', **app_conf):
128       
129        super(ApacheSSLAuthNMiddleware, self).__init__(app, 
130                                                       global_conf, 
131                                                       prefix=prefix,
132                                                       **app_conf)
133       
134        self.__caCertFilePathList = None
135        self.__caCertStack = None
136        self.__verifyClientCert = None
137       
138        self.rePathMatchList = [re.compile(r) for r in 
139                                app_conf.get('rePathMatchList','').split()]
140
141        self.caCertStack = app_conf.get('caCertFilePathList', [])
142       
143        # A custom class may be specified to determine what verification to
144        # apply to the client certificate
145        clientCertVerificationClassName = app_conf.get(
146                                    prefix+'clientCertVerificationClassName')
147        if clientCertVerificationClassName:
148            isClientCertVerificationProperty = lambda i: i[0].startswith(
149                                            prefix+'clientCertVerification.')
150            clientCertVerificationProperties = \
151                dict(filter(isClientCertVerificationProperty,app_conf.items()))
152               
153            self.verifyClientCert = instantiateClass(
154                             clientCertVerificationClassName, 
155                             None, 
156                             objectType=ClientCertVerificationInterface, 
157                             classProperties=clientCertVerificationProperties)           
158        else: 
159            # Default to carry out no verification
160            self.verifyClientCert = NoClientCertVerification()
161       
162    def _setCACertStack(self, caCertList):
163        '''Read CA certificates from file and add them to an X.509 Cert.
164        stack
165       
166        @type caCertList: basestring, list, tuple or
167        ndg.security.common.X509.X509Stack
168        @param caCertList: list of file paths for CA certificates to
169        be used to verify certificate used to sign message.  If a single
170        string, it will be parsed into a list based on space separator to
171        delimit items'''
172       
173        if isinstance(caCertList, X509Stack):
174            self.__caCertFilePathList = []
175            self.__caCertStack = caCertList
176            return
177       
178        else:
179            if isinstance(caCertList, basestring):
180                # Try parsing a space separated list of file paths
181                self.__caCertFilePathList = caCertList.split()
182               
183            elif isinstance(caCertList, (list, tuple)):
184                self.__caCertFilePathList = caCertList
185            else:
186                raise TypeError('Expecting a list or tuple for '
187                                '"caCertList"')
188   
189            self.__caCertStack = X509Stack()
190   
191            for caCertFilePath in self.__caCertFilePathList:
192                x509Cert = X509Cert.Read(os.path.expandvars(caCertFilePath))
193                self.__caCertStack.push(x509Cert)
194   
195    def _getCACertStack(self):
196        return self.__caCertStack
197
198    caCertStack = property(fset=_setCACertStack,
199                           fget=_getCACertStack,
200                           doc="CA certificate stack object - "
201                               "peer certificate must validate against one")
202   
203    def _getCACertFilePathList(self):
204        return self.__caCertFilePathList
205   
206    caCertFilePathList = property(fset=_setCACertStack,
207                                  fget=_getCACertFilePathList,
208                                  doc="list of CA certificate file paths - "
209                                      "peer certificate must validate against "
210                                      "one.  This property is set from the "
211                                      "caCertStack property assignment")
212
213    caCertStack = property(fset=_setCACertStack,
214                           fget=_getCACertStack,
215                           doc="CA certificate stack object - "
216                               "peer certificate must validate against one")
217   
218    def _setVerifyClientCert(self, value):
219        '''
220        @type value: ClientCertVerificationInterface derived type
221        @param value: custom SSL client verification interface
222        '''
223       
224        if not isinstance(value, ClientCertVerificationInterface):
225            raise TypeError('Expecting %r type for "verifyClientCert"; got '
226                            '%r' % type(value))
227       
228        if not callable(value):
229            raise TypeError('Expecting callable for "verifyClientCert"')
230       
231        self.__verifyClientCert = value
232   
233    def _getVerifyClientCert(self):
234        return self.__verifyClientCert
235
236    verifyClientCert = property(fset=_setVerifyClientCert,
237                                fget=_getVerifyClientCert,
238                                doc="Client certificate verification "
239                                    "interface object")
240   
241    @NDGSecurityMiddlewareBase.initCall         
242    def __call__(self, environ, start_response):
243        '''Check for peer certificate in environment and if present carry out
244        authentication'''
245        log.debug("ApacheSSLAuthNMiddleware.__call__ ...")
246       
247        if not self._pathMatch():
248            log.debug("ApacheSSLAuthNMiddleware: ignoring path [%s]", 
249                      self.pathInfo)
250            return self._setResponse()
251       
252        elif not self.isSSLRequest:
253            log.warning("ApacheSSLAuthNMiddleware: 'HTTPS' environment "
254                        "variable not found in environment; ignoring request")
255            return self._setResponse()
256                       
257        elif not self.isSSLClientCertSet:
258            log.error("ApacheSSLAuthNMiddleware: No SSL Client certificate "
259                      "for request to [%s]; setting HTTP 401 Unauthorized", 
260                      self.pathInfo)
261            return self._setErrorResponse(code=401,
262                                          msg='No client SSL Certificate set')
263           
264        if self.isValidClientCert():           
265            return self._setResponse()
266        else:
267            return self._setErrorResponse(code=401)
268
269           
270    def _setResponse(self, 
271                     notFoundMsg='No application set for '
272                                 'ApacheSSLAuthNMiddleware',
273                     **kw):
274        return super(ApacheSSLAuthNMiddleware, 
275                     self)._setResponse(notFoundMsg=notFoundMsg, **kw)
276
277    def _setErrorResponse(self, msg='Invalid SSL client certificate', **kw):
278        return super(ApacheSSLAuthNMiddleware, self)._setErrorResponse(msg=msg,
279                                                                       **kw)
280
281    def _pathMatch(self):
282        """Apply a list of regular expression matching patterns to the contents
283        of environ['PATH_INFO'], if any match, return True.  This method is
284        used to determine whether to apply SSL client authentication
285        """
286        path = self.pathInfo
287        for regEx in self.rePathMatchList:
288            if regEx.match(path):
289                return True
290           
291        return False
292   
293    def isValidClientCert(self):
294        sslClientCert = self.environ[
295                            ApacheSSLAuthNMiddleware.SSL_CLIENT_CERT_KEYNAME]
296        x509Cert = X509Cert.Parse(sslClientCert)
297       
298        if len(self.caCertStack) == 0:
299            log.warning("No CA certificates set for Client certificate "
300                        "signature verification")
301        else:
302            try:
303                self.caCertStack.verifyCertChain(x509Cert2Verify=x509Cert)
304
305            except X509CertError, e:
306                log.info("Client certificate verification failed with %s "
307                         "exception: %s" % (e.__class__, e))
308                return False
309           
310            except Exception, e:
311                log.error("Client certificate verification failed with "
312                          "unexpected exception type %s: %s" % (e.__class__,e))
313                return False
314       
315        # Check certificate Distinguished Name via
316        # ClientCertVerificationInterface object
317        return self.verifyClientCert(x509Cert)
318
Note: See TracBrowser for help on using the repository browser.