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

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

Added sqlalchemy module for SQLAlchemyAuthnInterface plugin to OpenID Provider.

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: %r environment variable "
263                        "not found in environment; ignoring request" % 
264                        self.sslKeyName)
265            return self._setResponse()
266                       
267        elif not self.isSSLClientCertSet:
268            log.error("ApacheSSLAuthnMiddleware: No SSL Client certificate "
269                      "for request to [%s]; setting HTTP 401 Unauthorized", 
270                      self.pathInfo)
271            return self._setErrorResponse(code=401,
272                                          msg='No client SSL Certificate set')
273           
274        if self.isValidClientCert():
275            self._setUser()         
276            return self._setResponse()
277        else:
278            return self._setErrorResponse(code=401)
279
280    def _setResponse(self, 
281                     notFoundMsg='No application set for '
282                                 'ApacheSSLAuthnMiddleware',
283                     **kw):
284        return super(ApacheSSLAuthnMiddleware, 
285                     self)._setResponse(notFoundMsg=notFoundMsg, **kw)
286
287    def _setErrorResponse(self, msg='Invalid SSL client certificate', **kw):
288        return super(ApacheSSLAuthnMiddleware, self)._setErrorResponse(msg=msg,
289                                                                       **kw)
290
291    def _pathMatch(self):
292        """Apply a list of regular expression matching patterns to the contents
293        of environ['PATH_INFO'], if any match, return True.  This method is
294        used to determine whether to apply SSL client authentication
295        """
296        path = self.pathInfo
297        for regEx in self.rePathMatchList:
298            if regEx.match(path):
299                return True
300           
301        return False
302       
303    def _isSSLClientCertSet(self):
304        """Check for SSL Certificate set in environ"""
305        sslClientCert = self.environ.get(
306                        self.sslClientCertKeyName, '')
307        return sslClientCert.startswith(
308                                    ApacheSSLAuthnMiddleware.PEM_CERT_PREFIX) 
309       
310    isSSLClientCertSet = property(fget=_isSSLClientCertSet,
311                                  doc="Check for client X.509 certificate "
312                                      "%r setting in environ" %
313                                      SSL_CLIENT_CERT_KEYNAME)
314   
315    def isValidClientCert(self):
316        sslClientCert = self.environ[self.sslClientCertKeyName]
317       
318        # Certificate string passed through a proxy has spaces in place of
319        # newline delimiters.  Fix by re-organising the string into a single
320        # line and remove the BEGIN CERTIFICATE / END CERTIFICATE delimiters.
321        # Then, treat as a base64 encoded string decoding and passing as DER
322        # format to the X.509 parser
323        x509CertPat = re.compile('(\s?-----[A-Z]+\sCERTIFICATE-----\s?)|\s+')
324        cert = x509CertPat.sub('', sslClientCert)
325        derCert = base64.decodestring(cert)
326        self.__clientCert = X509Cert.Parse(derCert, format=X509Cert.formatDER)
327       
328        if len(self.caCertStack) == 0:
329            log.warning("No CA certificates set for Client certificate "
330                        "signature verification")
331        else:
332            try:
333                self.caCertStack.verifyCertChain(
334                                            x509Cert2Verify=self.__clientCert)
335
336            except X509CertError, e:
337                log.info("Client certificate verification failed with %s "
338                         "exception: %s" % (type(e), e))
339                return False
340           
341            except Exception, e:
342                log.error("Client certificate verification failed with "
343                          "unexpected exception type %s: %s" % (type(e), e))
344                return False
345           
346        if len(self.clientCertDNMatchList) > 0:
347            dn = self.__clientCert.dn
348            for expectedDN in self.clientCertDNMatchList: 
349                if dn == expectedDN:
350                    return True
351               
352            return False
353           
354        return True
355
356    def _setUser(self):
357        """Interface hook for a derived class to set user ID from certificate
358        set or other context info.
359        """
360
361
362class AuthKitSSLAuthnMiddleware(ApacheSSLAuthnMiddleware):
363    """Update REMOTE_USER AuthKit environ key with certificate CommonName to
364    flag logged in status to other middleware and set cookie using Paste
365    Auth Ticket
366    """
367    SET_USER_ENVIRON_KEYNAME = 'paste.auth_tkt.set_user'
368   
369    @NDGSecurityMiddlewareBase.initCall         
370    def __call__(self, environ, start_response):
371        '''Check for peer certificate in environment and if present carry out
372        authentication.  Overrides parent class behaviour to set REMOTE_USER
373        AuthKit environ key based on the client certificate's Distinguished
374        Name CommonName field.  If no certificate is present or it is
375        present but invalid no 401 response is set.  Instead, it is left to
376        the following middleware in the chain to deal with this.  When used
377        in conjunction with
378        ndg.security.server.wsgi.openid.relyingparty.OpenIDRelyingPartyMiddleware,
379        this will result in the display of the Relying Party interface but with
380        a 401 status set.
381       
382        @type environ: dict
383        @param environ: WSGI environment variables dictionary
384        @type start_response: function
385        @param start_response: standard WSGI start response function
386        '''
387        if not self._pathMatch():
388            log.debug("AuthKitSSLAuthnMiddleware: ignoring path [%s]", 
389                      self.pathInfo)
390
391        elif not self.isSSLRequest:
392            log.debug("AuthKitSSLAuthnMiddleware: 'HTTPS' environment "
393                        "variable not found in environment; ignoring request")
394                       
395        elif not self.isSSLClientCertSet:
396            log.debug("AuthKitSSLAuthnMiddleware: no client certificate set - "
397                      "passing request to next middleware in the chain ...")
398           
399        elif self.isValidClientCert():
400            # Update session cookie with user ID
401            self._setUser()
402           
403        # ... isValidCert will log warnings/errors no need to flag the False
404        # condition
405           
406        # Pass request to next middleware in the chain without setting an
407        # error response - see method doc string for explanation.
408        return self._setResponse()
409
410    def _setUser(self):
411        """Set user ID in AuthKit cookie from client certificate submitted
412        """
413        userId = self.clientCert.dn['CN']
414       
415        self.environ[AuthKitSSLAuthnMiddleware.USERNAME_ENVIRON_KEYNAME]=userId
416        self.environ[AuthKitSSLAuthnMiddleware.SET_USER_ENVIRON_KEYNAME](userId)
Note: See TracBrowser for help on using the repository browser.