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

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

Adding SSL Client authentication step into authz_lite integration test. Broken redirecting back from authn step to requested resource.

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
19# Pattern matching to determine which URI paths to apply SSL AuthN to and to
20# parse SSL certificate environment variable
21import re 
22
23# Decode SSL certificate environment variable
24import base64       
25
26from ndg.security.server.wsgi import NDGSecurityMiddlewareBase
27from ndg.security.common.X509 import X509Stack, X509Cert, X509CertError, X500DN
28from ndg.security.common.utils.classfactory import instantiateClass
29   
30
31class ApacheSSLAuthnMiddleware(NDGSecurityMiddlewareBase):
32    """Perform SSL peer certificate authentication making use of Apache
33    SSL environment settings
34   
35    B{This class relies on SSL environment settings being present as available
36    when run embedded within Apache using for example mod_wsgi}
37   
38    - SSL Client certificate is expected to be present in environ as
39    SSL_CLIENT_CERT key as set by Apache SSL with ExportCertData option to
40    SSLOptions directive enabled.
41    """
42    SSL_KEYNAME = 'HTTPS'
43    SSL_KEYVALUES = ('1', 'on')
44   
45    _isSSLRequest = lambda self: self.environ.get(self.sslKeyName) in \
46                                    ApacheSSLAuthnMiddleware.SSL_KEYVALUES
47    isSSLRequest = property(fget=_isSSLRequest,
48                            doc="Is an SSL request boolean - depends on "
49                                "'HTTPS' Apache environment variable setting")
50   
51    SSL_CLIENT_CERT_KEYNAME = 'SSL_CLIENT_CERT'
52    PEM_CERT_PREFIX = '-----BEGIN CERTIFICATE-----'
53   
54    # Options for ini file
55    RE_PATH_MATCH_LIST_OPTNAME = 'rePathMatchList'
56    CACERT_FILEPATH_LIST_OPTNAME = 'caCertFilePathList'
57    CLIENT_CERT_DN_MATCH_LIST_OPTNAME = 'clientCertDNMatchList'
58    SSL_KEYNAME_OPTNAME = 'sslKeyName'
59    SSL_CLIENT_CERT_KEYNAME_OPTNAME = 'sslClientCertKeyName'
60   
61    propertyDefaults = {
62        RE_PATH_MATCH_LIST_OPTNAME: [],
63        CACERT_FILEPATH_LIST_OPTNAME: [],
64        CLIENT_CERT_DN_MATCH_LIST_OPTNAME: [],
65        SSL_KEYNAME_OPTNAME: SSL_KEYNAME,
66        SSL_CLIENT_CERT_KEYNAME_OPTNAME: SSL_CLIENT_CERT_KEYNAME
67    }
68    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
69   
70    PARAM_PREFIX = 'sslAuthn.'
71   
72    def __init__(self, app, global_conf, prefix=PARAM_PREFIX, **app_conf):
73       
74        super(ApacheSSLAuthnMiddleware, self).__init__(app, 
75                                                       global_conf, 
76                                                       prefix=prefix,
77                                                       **app_conf)
78
79        self.__caCertFilePathList = None
80        self.__caCertStack = None
81        self.__clientCertDNMatchList = None
82        self.__clientCert = None
83        self.__sslClientCertKeyName = None
84        self.__sslKeyName = None
85       
86        rePathMatchListParamName = prefix + \
87                    ApacheSSLAuthnMiddleware.RE_PATH_MATCH_LIST_OPTNAME
88        rePathMatchListVal = app_conf.get(rePathMatchListParamName, '')
89       
90        self.rePathMatchList = [re.compile(r) 
91                                for r in rePathMatchListVal.split()]
92       
93        caCertFilePathListParamName = prefix + \
94                    ApacheSSLAuthnMiddleware.CACERT_FILEPATH_LIST_OPTNAME
95                       
96        self.caCertStack = app_conf.get(caCertFilePathListParamName, [])
97       
98        clientCertDNMatchListParamName = prefix + \
99                    ApacheSSLAuthnMiddleware.CLIENT_CERT_DN_MATCH_LIST_OPTNAME
100                   
101        self.clientCertDNMatchList = app_conf.get(
102                                        clientCertDNMatchListParamName, [])
103       
104        sslClientCertParamName = prefix + \
105                    ApacheSSLAuthnMiddleware.SSL_CLIENT_CERT_KEYNAME_OPTNAME   
106        self.sslClientCertKeyName = app_conf.get(sslClientCertParamName, 
107                            ApacheSSLAuthnMiddleware.SSL_CLIENT_CERT_KEYNAME)
108       
109        sslKeyNameParamName = prefix + \
110                    ApacheSSLAuthnMiddleware.SSL_KEYNAME_OPTNAME   
111        self.sslKeyName = app_conf.get(sslKeyNameParamName, 
112                                       ApacheSSLAuthnMiddleware.SSL_KEYNAME)
113
114    def _getSslClientCertKeyName(self):
115        return self.__sslClientCertKeyName
116
117    def _setSslClientCertKeyName(self, value):
118        if not isinstance(value, basestring):
119            raise TypeError('Expecting %r type for "sslClientCertKeyName"; '
120                            'got %r' % (basestring, type(value)))
121        self.__sslClientCertKeyName = value
122
123    sslClientCertKeyName = property(_getSslClientCertKeyName, 
124                                    _setSslClientCertKeyName, 
125                                    doc="SslClientCertKeyName's Docstring")
126
127    def _getSslKeyName(self):
128        return self.__sslKeyName
129
130    def _setSslKeyName(self, value):       
131        if not isinstance(value, basestring):
132            raise TypeError('Expecting %r type for "sslKeyName"; got %r' %
133                            (basestring, type(value)))
134        self.__sslKeyName = value
135
136    sslKeyName = property(_getSslKeyName, 
137                          _setSslKeyName, 
138                          doc="SslKeyName's Docstring")
139
140               
141    def _setCACertStack(self, caCertList):
142        '''Read CA certificates from file and add them to an X.509 Cert.
143        stack
144       
145        @type caCertList: basestring, list, tuple or
146        ndg.security.common.X509.X509Stack
147        @param caCertList: list of file paths for CA certificates to
148        be used to verify certificate used to sign message.  If a single
149        string, it will be parsed into a list based on space separator to
150        delimit items'''
151       
152        if isinstance(caCertList, X509Stack):
153            self.__caCertFilePathList = []
154            self.__caCertStack = caCertList
155            return
156       
157        else:
158            if isinstance(caCertList, basestring):
159                # Try parsing a space separated list of file paths
160                self.__caCertFilePathList = caCertList.split()
161               
162            elif isinstance(caCertList, (list, tuple)):
163                self.__caCertFilePathList = caCertList
164            else:
165                raise TypeError('Expecting a list or tuple for '
166                                '"caCertList"')
167   
168            self.__caCertStack = X509Stack()
169   
170            for caCertFilePath in self.__caCertFilePathList:
171                x509Cert = X509Cert.Read(os.path.expandvars(caCertFilePath))
172                self.__caCertStack.push(x509Cert)
173   
174    def _getCACertStack(self):
175        return self.__caCertStack
176
177    caCertStack = property(fset=_setCACertStack,
178                           fget=_getCACertStack,
179                           doc="CA certificate stack object - "
180                               "peer certificate must validate against one")
181   
182    def _getCACertFilePathList(self):
183        return self.__caCertFilePathList
184   
185    caCertFilePathList = property(fset=_setCACertStack,
186                                  fget=_getCACertFilePathList,
187                                  doc="list of CA certificate file paths - "
188                                      "peer certificate must validate against "
189                                      "one.  This property is set from the "
190                                      "caCertStack property assignment")
191
192    caCertStack = property(fset=_setCACertStack,
193                           fget=_getCACertStack,
194                           doc="CA certificate stack object - "
195                               "peer certificate must validate against one")
196       
197    def _setClientCertDNMatchList(self, value):
198        '''       
199        @type value: basestring, list, tuple
200        @param value: list of client certificate Distinguished Names as strings
201        of X500DN instances'''
202       
203        if isinstance(value, basestring):
204            # Try parsing a space separated list of file paths
205            self.__clientCertDNMatchList = [X500DN(dn=dn) 
206                                            for dn in value.split()]
207           
208        elif isinstance(value, (list, tuple)):
209            self.__clientCertDNMatchList = []
210            for dn in value:
211                if isinstance(dn, basestring):
212                    self.__clientCertDNMatchList.append(X500DN(dn=dn))
213                elif isinstance(dn, X500DN):
214                    self.__clientCertDNMatchList.append(dn)
215                else:
216                    raise TypeError('Expecting a string, or %r type for "%s" '
217                                    'list item; got %r' % 
218                (X500DN,
219                 ApacheSSLAuthnMiddleware.CLIENT_CERT_DN_MATCH_LIST_OPTNAME,
220                 type(dn)))
221                   
222        else:
223            raise TypeError('Expecting a string, list or tuple for "%s"; got '
224                            '%r' % 
225                (ApacheSSLAuthnMiddleware.CLIENT_CERT_DN_MATCH_LIST_OPTNAME,
226                 type(value)))
227   
228    def _getClientCertDNMatchList(self):
229        return self.__clientCertDNMatchList
230
231    clientCertDNMatchList = property(fset=_setClientCertDNMatchList,
232                                     fget=_getClientCertDNMatchList,
233                                     doc="List of acceptable Distinguished "
234                                         "Names for client certificates")
235       
236    def _getClientCert(self):
237        return self.__clientCert
238
239    clientCert = property(fget=_getClientCert,
240                          doc="Client certificate for verification set by "
241                              "isValidClientCert()")
242
243   
244    @NDGSecurityMiddlewareBase.initCall         
245    def __call__(self, environ, start_response):
246        '''Check for peer certificate in environment and if present carry out
247        authentication
248       
249        @type environ: dict
250        @param environ: WSGI environment variables dictionary
251        @type start_response: function
252        @param start_response: standard WSGI start response function
253        '''
254        log.debug("ApacheSSLAuthnMiddleware.__call__ ...")
255       
256        if not self._pathMatch():
257            log.debug("ApacheSSLAuthnMiddleware: ignoring path [%s]", 
258                      self.pathInfo)
259            return self._setResponse()
260       
261        elif not self.isSSLRequest:
262            log.warning("ApacheSSLAuthnMiddleware: 'HTTPS' environment "
263                        "variable not found in environment; ignoring request")
264            return self._setResponse()
265                       
266        elif not self.isSSLClientCertSet:
267            log.error("ApacheSSLAuthnMiddleware: No SSL Client certificate "
268                      "for request to [%s]; setting HTTP 401 Unauthorized", 
269                      self.pathInfo)
270            return self._setErrorResponse(code=401,
271                                          msg='No client SSL Certificate set')
272           
273        if self.isValidClientCert():           
274            return self._setResponse()
275        else:
276            return self._setErrorResponse(code=401)
277
278    def _setResponse(self, 
279                     notFoundMsg='No application set for '
280                                 'ApacheSSLAuthnMiddleware',
281                     **kw):
282        return super(ApacheSSLAuthnMiddleware, 
283                     self)._setResponse(notFoundMsg=notFoundMsg, **kw)
284
285    def _setErrorResponse(self, msg='Invalid SSL client certificate', **kw):
286        return super(ApacheSSLAuthnMiddleware, self)._setErrorResponse(msg=msg,
287                                                                       **kw)
288
289    def _pathMatch(self):
290        """Apply a list of regular expression matching patterns to the contents
291        of environ['PATH_INFO'], if any match, return True.  This method is
292        used to determine whether to apply SSL client authentication
293        """
294        path = self.pathInfo
295        for regEx in self.rePathMatchList:
296            if regEx.match(path):
297                return True
298           
299        return False
300       
301    def _isSSLClientCertSet(self):
302        """Check for SSL Certificate set in environ"""
303        sslClientCert = self.environ.get(
304                        self.sslClientCertKeyName, '')
305        return sslClientCert.startswith(
306                                    ApacheSSLAuthnMiddleware.PEM_CERT_PREFIX) 
307       
308    isSSLClientCertSet = property(fget=_isSSLClientCertSet,
309                                  doc="Check for client X.509 certificate "
310                                      "%r setting in environ" %
311                                      SSL_CLIENT_CERT_KEYNAME)
312   
313    def isValidClientCert(self):
314        sslClientCert = self.environ[self.sslClientCertKeyName]
315       
316        # Certificate string passed through a proxy has spaces in place of
317        # newline delimiters.  Fix by re-organising the string into a single
318        # line and remove the BEGIN CERTIFICATE / END CERTIFICATE delimiters.
319        # Then, treat as a base64 encoded string decoding and passing as DER
320        # format to the X.509 parser
321        x509CertPat = re.compile('(\s?-----[A-Z]+\sCERTIFICATE-----\s?)|\s+')
322        cert = x509CertPat.sub('', sslClientCert)
323        derCert = base64.decodestring(cert)
324        self.__clientCert = X509Cert.Parse(derCert, format=X509Cert.formatDER)
325       
326        if len(self.caCertStack) == 0:
327            log.warning("No CA certificates set for Client certificate "
328                        "signature verification")
329        else:
330            try:
331                self.caCertStack.verifyCertChain(
332                                            x509Cert2Verify=self.__clientCert)
333
334            except X509CertError, e:
335                log.info("Client certificate verification failed with %s "
336                         "exception: %s" % (type(e), e))
337                return False
338           
339            except Exception, e:
340                log.error("Client certificate verification failed with "
341                          "unexpected exception type %s: %s" % (type(e), e))
342                return False
343           
344        if len(self.clientCertDNMatchList) > 0:
345            if self.__clientCert.dn not in self.clientCertDNMatchList:
346                return False
347           
348        return True
349   
350
351class AuthKitSSLAuthnMiddleware(ApacheSSLAuthnMiddleware):
352    """Update REMOTE_USER AuthKit environ key with certificate CommonName to
353    flag logged in status to other middleware
354    """
355    @NDGSecurityMiddlewareBase.initCall         
356    def __call__(self, environ, start_response):
357        '''Check for peer certificate in environment and if present carry out
358        authentication.  Overrides parent class behaviour to set REMOTE_USER
359        AuthKit environ key based on the client certificate's Distinguished
360        Name CommonName field.  If no certificate is present or it is
361        present but invalid no 401 response is set.  Instead, it is left to
362        the following middleware in the chain to deal with this.  When used
363        in conjunction with
364        ndg.security.server.wsgi.openid.relyingparty.OpenIDRelyingPartyMiddleware,
365        this will result in the display of the Relying Party interface but with
366        a 401 status set.
367       
368        @type environ: dict
369        @param environ: WSGI environment variables dictionary
370        @type start_response: function
371        @param start_response: standard WSGI start response function
372        '''
373        if not self._pathMatch():
374            log.debug("AuthKitSSLAuthnMiddleware: ignoring path [%s]", 
375                      self.pathInfo)
376
377        elif not self.isSSLRequest:
378            log.debug("AuthKitSSLAuthnMiddleware: 'HTTPS' environment "
379                        "variable not found in environment; ignoring request")
380                       
381        elif not self.isSSLClientCertSet:
382            log.debug("AuthKitSSLAuthnMiddleware: no client certificate set - "
383                      "passing request to next middleware in the chain ...")
384           
385        elif self.isValidClientCert():
386            # Update environ so that downstream AuthenticationMiddleware can
387            # set the session cookie
388            self.environ['REMOTE_USER'] = self.clientCert.dn['CN']
389           
390            # Set-up redirect back to original request URI
391        else:
392            # IsValidCert will log warnings/errors no need to flag this
393            # condition
394            pass
395           
396        # Pass request to next middleware in the chain without setting an
397        # error response - see method doc string for explanation.
398        return self._setResponse()
Note: See TracBrowser for help on using the repository browser.