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

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/ssl.py@5357
Revision 5357, 10.7 KB checked in by pjkersha, 11 years ago (diff)
  • fix to WS-Security signature handler 4Suite implementation (ndg.security.common.wssecurity.signaturehandler.foursuite) to ensure timestamp is checked correctly
  • refactored ndg.security.common.wssecurity moving encryption handler development code into its own ndg.security.common.wssecurity.encryptionhandler package
  • Fixed copyright on some remaining files that still had NERC/CCLRC
  • further work on SSL CLient AuthN WSGI unit tests ndg.security.test.unit.wsgi.ssl
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    sslKeyName = 'HTTPS'
103
104    _isSSLRequest = lambda self: self.environ.get(
105                                ApacheSSLAuthNMiddleware.sslKeyName) == '1'
106    isSSLRequest = property(fget=_isSSLRequest,
107                            doc="Is an SSL request boolean - depends on "
108                                "'HTTPS' Apache environment variable setting")
109   
110    sslClientCertKeyName = 'SSL_CLIENT_CERT'
111   
112    propertyDefaults = {
113        'clientCertVerificationClassName': None,
114        'rePathMatchList': [],
115        'caCertFilePathList': []
116    }
117    propertyDefaults.update(NDGSecurityMiddlewareBase.propertyDefaults)
118       
119    _isSSLClientCertSet = lambda self: bool(self.environ.get(
120                                ApacheSSLAuthNMiddleware.sslClientCertKeyName)) 
121    isSSLClientCertSet = property(fget=_isSSLClientCertSet,
122                                  doc="Check for client X.509 certificate "
123                                      "'SSL_CLIENT_CERT' setting in environ")
124   
125    def __init__(self, app, global_conf, prefix='', **app_conf):
126       
127        super(ApacheSSLAuthNMiddleware, self).__init__(app, 
128                                                       global_conf, 
129                                                       prefix=prefix,
130                                                       **app_conf)
131       
132        self.rePathMatchList = [re.compile(r) for r in 
133                                app_conf.get('rePathMatchList','').split()]
134
135        self.caCertFilePathList = app_conf.get('caCertFilePathList', [])
136       
137        # A custom class may be specified to determine what verification to
138        # apply to the client certificate
139        clientCertVerificationClassName = app_conf.get(
140                                    prefix+'clientCertVerificationClassName')
141        if clientCertVerificationClassName:
142            isClientCertVerificationProperty = lambda i: i[0].startswith(
143                                            prefix+'clientCertVerification.')
144            clientCertVerificationProperties = \
145                dict(filter(isClientCertVerificationProperty,app_conf.items()))
146               
147            self._verifyClientCert = instantiateClass(
148                             clientCertVerificationClassName, 
149                             None, 
150                             objectType=ClientCertVerificationInterface, 
151                             classProperties=clientCertVerificationProperties)           
152        else: 
153            # Default to carry out no verification
154            self._verifyClientCert = NoClientCertVerification
155       
156       
157    def _setCACertsFromFileList(self, caCertFilePathList):
158        '''Read CA certificates from file and add them to an X.509 Cert.
159        stack
160       
161        @type caCertFilePathList: list or tuple
162        @param caCertFilePathList: list of file paths for CA certificates to
163        be used to verify certificate used to sign message'''
164       
165        if isinstance(caCertFilePathList, basestring):
166            # Try parsing a space separated list of file paths
167            caCertFilePathList = caCertFilePathList.split()
168           
169        elif not isinstance(caCertFilePathList, (list, tuple)):
170            raise TypeError('Expecting a list or tuple for '
171                            '"caCertFilePathList"')
172
173        self._caCertFilePathList = caCertFilePathList
174        self._caCertStack = X509Stack()
175
176        for caCertFilePath in caCertFilePathList:
177            x509Cert = X509Cert.Read(os.path.expandvars(caCertFilePath))
178            self._caCertStack.push(x509Cert)
179   
180    def _getCACertFilePathList(self):
181        return self._caCertFilePathList
182   
183    caCertFilePathList = property(fset=_setCACertsFromFileList,
184                                  fget=_getCACertFilePathList,
185                                  doc="list of CA certificate file paths - "
186                                      "peer certificate must validate against "
187                                      "one")
188     
189    @NDGSecurityMiddlewareBase.initCall         
190    def __call__(self, environ, start_response):
191        '''Check for peer certificate in environment and if present carry out
192        authentication'''
193        log.debug("ApacheSSLAuthNMiddleware.__call__ ...")
194       
195        if not self.isSSLRequest:
196            log.warning("ApacheSSLAuthNMiddleware: 'HTTPS' environment "
197                        "variable not found in environment; ignoring request")
198            return self._setResponse()
199           
200        elif not self._pathMatch():
201            log.debug("ApacheSSLAuthNMiddleware: ignoring path [%s]", 
202                      self.pathInfo)
203            return self._setResponse()
204                   
205        elif not self.isSSLClientCertSet:
206            log.error("ApacheSSLAuthNMiddleware: No SSL Client certificate "
207                      "for request to [%s]; setting HTTP 401 Unauthorized", 
208                      self.pathInfo)
209            return self._setErrorResponse(code=401,
210                                          msg='No client SSL Certificate set')
211           
212        if self.isValidClientCert():           
213            return self._setResponse()
214        else:
215            return self._setErrorResponse(code=401)
216
217           
218    def _setResponse(self, 
219                     notFoundMsg='No application set for '
220                                 'ApacheSSLAuthNMiddleware',
221                     **kw):
222        return super(ApacheSSLAuthNMiddleware, 
223                     self)._setResponse(notFoundMsg=notFoundMsg)
224
225    def _setErrorResponse(self, msg='Invalid SSL client certificate', **kw):
226        return super(ApacheSSLAuthNMiddleware, self)._setErrorResponse(msg=msg,
227                                                                       **kw)
228
229    def _pathMatch(self):
230        """Apply a list of regular expression matching patterns to the contents
231        of environ['PATH_INFO'], if any match, return True.  This method is
232        used to determine whether to apply SSL client authentication
233        """
234        path = self.pathInfo
235        for regEx in self.rePathMatchList:
236            if regEx.match(path):
237                return True
238           
239        return False
240   
241    def isValidClientCert(self):
242        sslClientCert = self.environ[
243                                ApacheSSLAuthNMiddleware.sslClientCertKeyName]
244        x509Cert = X509Cert.Parse(sslClientCert)
245       
246        if len(self._caCertStack) == 0:
247            log.warning("No CA certificates set for Client certificate "
248                        "signature verification")
249        else:
250            try:
251                self._caCertStack.verifyCertChain(x509Cert2Verify=x509Cert)
252
253            except X509CertError, e:
254                log.info("Client certificate verification failed with %s "
255                         "exception: %s" % (e.__class__, e))
256                return False
257           
258            except Exception, e:
259                log.error("Client certificate verification failed with "
260                          "unexpected exception type %s: %s" % (e.__class__,e))
261                return False
262       
263        # Check certificate Distinguished Name via
264        # ClientCertVerificationInterface object
265        return self._verifyClientCert(x509Cert)
266
Note: See TracBrowser for help on using the repository browser.