Changeset 5499


Ignore:
Timestamp:
21/07/09 09:49:41 (10 years ago)
Author:
pjkersha
Message:
  • Added IdP ValidationDriver? specialisation for SSL peer cert verification using M2Crypto
  • completed tests for OpenID Relying Party IdP validation - ndg.security.test.unit.openid.relyingparty.validation
Location:
TI12-security/trunk/python
Files:
2 added
5 edited

Legend:

Unmodified
Added
Removed
  • TI12-security/trunk/python/ndg.security.common/ndg/security/common/utils/classfactory.py

    r5497 r5499  
    4949                        (className, objectType, importedClass)) 
    5050     
    51     log.info('Imported "%s" class from module, "%s"', className, moduleName) 
     51    log.info('Imported "%s" class from module, "%s"', className, _moduleName) 
    5252    return importedClass 
    5353     
  • TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/openid/relyingparty/__init__.py

    r5372 r5499  
    331331                      callback=lambda *arg, **kw: self.priKeyPwd) 
    332332     
     333        # Force Python OpenID library to use Urllib2 fetcher instead of the  
     334        # Curl based one otherwise the M2Crypto SSL handler will be ignored. 
    333335        setDefaultFetcher(Urllib2Fetcher()) 
    334336         
  • TI12-security/trunk/python/ndg.security.server/ndg/security/server/wsgi/openid/relyingparty/validation.py

    r5497 r5499  
    7070    """Parser for IdP and Attribute Provider config 
    7171    """ 
    72     @staticmethod 
    73     def getNamedElem(name):  
    74         for e in elem: 
    75             if e == name: 
    76                 return e 
    77         return None 
    7872     
    7973    def getValidators(self, source):   
     
    125119                validatorConfig.className = elem.attrib["name"] 
    126120                 
     121                parameters = {} 
    127122                for el in elem: 
    128                     parameters = {} 
    129123                    if getLocalName(el).lower() == "parameter": 
    130124                        if el.attrib["name"] in parameters: 
     
    194188        @raise ConfigException:'''  
    195189        raise NotImplementedError() 
    196      
     190  
     191 
     192class SSLIdPValidator(object): 
     193    '''Interface class for implementing OpenID Provider validators for a 
     194    Relying Party to call''' 
     195     
     196    def __init__(self): 
     197        raise NotImplementedError() 
     198 
     199    def initialize(self, **parameters): 
     200        '''@raise ConfigException:'''  
     201        raise NotImplementedError() 
     202        
     203    def validate(self, x509CertCtx): 
     204        '''@type x509StoreCtx: M2Crypto.X509_Store_Context 
     205        @param x509StoreCtx: context object containing peer certificate and 
     206        certificate chain for verification 
     207  
     208        @raise IdPInvalidException: 
     209        @raise ConfigException:'''  
     210        raise NotImplementedError() 
     211     
     212     
     213import urllib2 
     214from M2Crypto import SSL 
     215from M2Crypto.m2urllib2 import build_opener 
     216from ndg.security.common.X509 import X500DN 
     217 
     218class SSLClientAuthNValidator(SSLIdPValidator): 
     219    parameters = { 
     220        'configFilePath': basestring, 
     221        'caCertDirPath': basestring, 
     222        'certFilePath': basestring, 
     223        'priKeyFilePath': basestring, 
     224        'priKeyPwd': basestring 
     225    } 
     226     
     227    def __init__(self): 
     228        """Set-up default SSL context for HTTPS requests""" 
     229        for p in SSLClientAuthNValidator.parameters: 
     230            setattr(self, p, None) 
     231             
     232    def initialize(self, ctx, **parameters): 
     233        '''@raise ConfigException:'''  
     234        for name, val in parameters.items(): 
     235            if name not in SSLClientAuthNValidator.parameters: 
     236                raise AttributeError('Invalid parameter name "%s".  Valid ' 
     237                                     'names are %r' % (name, 
     238                                    SSLClientAuthNValidator.parameters.keys())) 
     239                 
     240            if not isinstance(val, SSLClientAuthNValidator.parameters[name]): 
     241                raise TypeError('Invalid type %r for parameter "%s" expecting ' 
     242                                '%r ' %  
     243                                (val.__class__,  
     244                                 name,  
     245                                 SSLClientAuthNValidator.parameters[name])) 
     246         
     247            setattr(self, name, os.path.expandvars(val)) 
     248              
     249        ctx.load_verify_locations(capath=self.caCertDirPath) 
     250        if self.certFilePath is not None and self.priKeyFilePath is not None: 
     251            ctx.load_cert(self.certFilePath,  
     252                          keyfile=self.priKeyFilePath,  
     253                          callback=lambda *arg, **kw: self.priKeyPwd) 
     254             
     255        if self.configFilePath is not None: 
     256            # Simple file format - one IdP server name per line 
     257            cfgFile = open(self.configFilePath) 
     258            self.validIdPNames = [l.strip() for l in cfgFile.readlines() 
     259                                  if not l.startswith('#')] 
     260                           
     261    def validate(self, x509StoreCtx): 
     262        '''callback function used to control the behaviour when the  
     263        SSL_VERIFY_PEER flag is set 
     264         
     265        @type x509StoreCtx: M2Crypto.X509_Store_Context 
     266        @param x509StoreCtx: locate the certificate to be verified and perform  
     267        additional verification steps as needed 
     268        @rtype: int 
     269        @return: controls the strategy of the further verification process.  
     270        - If verify_callback returns 0, the verification process is immediately  
     271        stopped with "verification failed" state. If SSL_VERIFY_PEER is set,  
     272        a verification failure alert is sent to the peer and the TLS/SSL  
     273        handshake is terminated.  
     274        - If verify_callback returns 1, the verification process is continued.  
     275        If verify_callback always returns 1, the TLS/SSL handshake will not be  
     276        terminated with respect to verification failures and the connection  
     277        will be established. The calling process can however retrieve the error 
     278        code of the last verification error using SSL_get_verify_result(3) or  
     279        by maintaining its own error storage managed by verify_callback. 
     280        ''' 
     281         
     282        x509Cert = x509StoreCtx.get_current_cert() 
     283        dnTxt = x509Cert.get_subject().as_text() 
     284        commonName = X500DN(dn=dnTxt)['CN'] 
     285 
     286        # If all is OK preVerifyOK will be 1.  Return this to the caller to 
     287        # that it's OK to proceed 
     288        if commonName not in self.validIdPNames: 
     289            raise IdPInvalidException("Peer certificate CN=%s is not in list " 
     290                                      "of valid OpenID Providers" % commonName) 
    197291 
    198292class IdPValidationDriver(object): 
    199293    """Parse an XML Validation configuration containing XML Validators and  
    200294    execute these against the Provider (IdP) input"""    
     295    idPValidatorBaseClass = IdPValidator 
    201296     
    202297    def __init__(self): 
    203         self._idPValidators = None 
     298        self._idPValidators = [] 
    204299         
    205300    def _get_idPValidators(self): 
     
    207302     
    208303    def _set_idPValidators(self, idPValidators): 
     304        badValidators = [i for i in idPValidators  
     305                         if not isinstance(i,  
     306                                        self.__class__.idPValidatorBaseClass)] 
     307        if len(badValidators): 
     308            raise TypeError("Input validators must be of IdPValidator derived " 
     309                            "type") 
     310                 
    209311        self._idPValidators = idPValidators 
    210312         
     
    220322             
    221323        if idpConfigFilePath is None: 
    222             log.warning("IdPValidationDriver.performIdPValidation: No IdP " 
     324            log.warning("IdPValidationDriver.readConfig: No IdP " 
    223325                        "Configuration file was set") 
    224326            return validators 
     
    227329        validatorConfigs = configReader.getValidators(idpConfigFilePath) 
    228330 
    229         for idpConfig in validatorConfigs: 
    230          
    231             className = idpConfig.className 
    232             parameters = idpConfig.parameters 
    233  
     331        for validatorConfig in validatorConfigs: 
    234332            try: 
    235                 validator = instantiateClass(className, 
     333                validator = instantiateClass(validatorConfig.className, 
    236334                                             None,  
    237335                                             objectType=IdPValidator) 
    238                 validator.initialize(**parameters) 
     336                validator.initialize(**validatorConfig.parameters) 
    239337                validators.append(validator) 
    240338             
    241339            except Exception, e:   
    242                 log.error("Failed to initialise validator: ", e) 
     340                log.error("Failed to initialise validator %s: %s",  
     341                          validatorConfig.className, 
     342                          e) 
    243343                 
    244344        return validators 
     
    292392        return discoveries 
    293393 
    294     __call__ = performIdPValidation 
    295      
    296      
    297 from openid.fetchers import USER_AGENT, _allowedURL, Urllib2Fetcher 
    298 import urllib2 
    299 from M2Crypto.m2urllib2 import HTTPSHandler 
    300 from M2Crypto import SSL 
    301 from M2Crypto.X509 import X509_Store_Context 
    302  
    303 class SSLClientAuthNValidator(IdPValidator): 
    304     parameters = { 
    305         'preVerifyOK': int,  
    306         'x509StoreCtx': M2Crypto.X509_Store_Context 
    307     } 
    308     def __init__(self): 
    309         raise NotImplementedError() 
    310  
    311     def initialize(self, **parameters): 
    312         '''@raise ConfigException:'''  
    313         for name, val in parameters.items(): 
    314             if name not in SSLClientAuthNValidator.parameters: 
    315                 raise AttributeError('Invalid parameter name "%s".  Valid ' 
    316                                      'names are %r' % (name, 
    317                                     SSLClientAuthNValidator.parameters)) 
    318             if not isinstance(val, parameters[name]): 
    319                 raise TypeError() 
    320         
    321     def validate(self, idpEndpoint, idpIdentity): 
    322         '''@raise IdPInvalidException: 
    323         @raise ConfigException:'''  
    324         raise NotImplementedError() 
    325  
    326     def verifyCallback(preVerifyOK, x509StoreCtx): 
    327         '''callback function used to control the behaviour when the  
    328         SSL_VERIFY_PEER flag is set 
    329          
    330         http://www.openssl.org/docs/ssl/SSL_CTX_set_verify.html 
    331          
    332         @type preVerifyOK: int 
     394 
     395class SSLIdPValidationDriver(IdPValidationDriver): 
     396    '''Validate an IdP using the certificate it returns from an SSL based 
     397    request''' 
     398    idPValidatorBaseClass = SSLIdPValidator 
     399     
     400    def __init__(self, idpConfigFilePath=None): 
     401        super(SSLIdPValidationDriver, self).__init__() 
     402         
     403        # Context object determines what validation is applied against the 
     404        # peer's certificate in the SSL connection 
     405        self.ctx = SSL.Context() 
     406         
     407        # Enforce peer cert checking via this classes' __call__ method 
     408        self.ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert,  
     409                            9,  
     410                            callback=self) 
     411 
     412        urllib2.install_opener(build_opener(ssl_context=self.ctx)) 
     413         
     414        if idpConfigFilePath is not None: 
     415            self.idPValidators += \ 
     416                self.readConfig(idpConfigFilePath=idpConfigFilePath) 
     417             
     418        log.info("%d IdPValidator(s) initialised", len(self.idPValidators)) 
     419         
     420    def readConfig(self, idpConfigFilePath): 
     421        """Read and initialise validators set in a config file"""   
     422        validators = [] 
     423         
     424        if idpConfigFilePath is None: 
     425            idpConfigFilePath = os.environ('SSL_IDP_VALIDATION_CONFIG_FILE') 
     426             
     427        if idpConfigFilePath is None: 
     428            log.warning("SSLIdPValidationDriver.readConfig: No IdP " 
     429                        "Configuration file was set") 
     430            return validators 
     431         
     432        configReader = XmlConfigReader() 
     433        validatorConfigs = configReader.getValidators(idpConfigFilePath) 
     434 
     435        for validatorConfig in validatorConfigs: 
     436            try: 
     437                validator = instantiateClass(validatorConfig.className, 
     438                                             None,  
     439                                             objectType=SSLIdPValidator) 
     440                 
     441                # Validator has access to the SSL context object in addition 
     442                # to custom settings set in parameters 
     443                validator.initialize(self.ctx, **validatorConfig.parameters) 
     444                validators.append(validator) 
     445             
     446            except Exception, e:   
     447                raise ConfigException("Validator class %r initialisation " 
     448                                      "failed with %s exception: %s" % 
     449                                      (validatorConfig.className,  
     450                                       e.__class__.__name__, 
     451                                       e)) 
     452                 
     453        return validators 
     454            
     455    def __call__(self, preVerifyOK, x509StoreCtx): 
     456        '''@type preVerifyOK: int 
    333457        @param preVerifyOK: If a verification error is found, this parameter  
    334458        will be set to 0 
    335459        @type x509StoreCtx: M2Crypto.X509_Store_Context 
    336         @param x509StoreCtx: locate the certificate to be verified and perform  
    337         additional verification steps as needed 
    338         @rtype: int 
    339         @return: controls the strategy of the further verification process.  
    340         - If verify_callback returns 0, the verification process is immediately  
    341         stopped with "verification failed" state. If SSL_VERIFY_PEER is set,  
    342         a verification failure alert is sent to the peer and the TLS/SSL  
    343         handshake is terminated.  
    344         - If verify_callback returns 1, the verification process is continued.  
    345         If verify_callback always returns 1, the TLS/SSL handshake will not be  
    346         terminated with respect to verification failures and the connection  
    347         will be established. The calling process can however retrieve the error 
    348         code of the last verification error using SSL_get_verify_result(3) or  
    349         by maintaining its own error storage managed by verify_callback. 
     460        @param x509StoreCtx: context object containing peer certificate and 
     461        certificate chain for verification 
    350462        ''' 
    351463        if preVerifyOK == 0: 
    352464            # Something is wrong with the certificate don't bother proceeding 
    353465            # any further 
     466            log.info("No custom Validation executed: a previous verification " 
     467                     "error has occurred") 
    354468            return preVerifyOK 
    355          
    356         x509Cert = x509StoreCtx.get_current_cert() 
    357         x509Cert.get_subject() 
    358         x509CertChain = x509StoreCtx.get1_chain() 
    359         for cert in x509CertChain: 
    360             subject = cert.get_subject() 
    361             dn = subject.as_text() 
    362             print dn 
    363              
    364         # If all is OK preVerifyOK will be 1.  Return this to the caller to 
    365         # that it's OK to proceed 
    366         return preVerifyOK 
    367          
    368     ctx = SSL.Context() 
    369  
    370     caDirPath = '../ndg.security.test/ndg/security/test/config/ca' 
    371     ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert,  
    372                    9,  
    373                    callback=verifyCallback) 
    374 #    ctx.set_verify(SSL.verify_peer|SSL.verify_fail_if_no_peer_cert, 1) 
    375  
    376     ctx.load_verify_locations(capath=caDirPath) 
    377 #    ctx.load_cert(certFilePath,  
    378 #                  keyfile=priKeyFilePath,  
    379 #                  callback=lambda *arg, **kw: priKeyPwd) 
    380  
    381     from M2Crypto.m2urllib2 import build_opener 
    382     urllib2.install_opener(build_opener(ssl_context=ctx)) 
    383  
    384  
     469 
     470        # validate the discovered end points 
     471        for validator in self.idPValidators:    
     472            try: 
     473                validator.validate(x509StoreCtx) 
     474                log.info("Whitelist Validator %s succeeded",  
     475                         validator.__class__.__name__) 
     476             
     477            except Exception, e: 
     478                log.error("Whitelist Validator %r caught %s exception with " 
     479                          "peer certificate context: %s",  
     480                          validator.__class__.__name__,  
     481                          e.__class__.__name__, 
     482                          e)        
     483                return 0 
     484             
     485        if len(self.idPValidators) == 0: 
     486            log.warning("No IdP validation executed because no validators " 
     487                        "were set") 
     488             
     489        return 1 
     490         
  • TI12-security/trunk/python/ndg.security.test/ndg/security/test/unit/__init__.py

    r5343 r5499  
    1717from os.path import expandvars, join, dirname, abspath 
    1818 
     19TEST_CONFIG_DIR = join(abspath(dirname(dirname(__file__))), 'config') 
    1920 
    2021class BaseTestCase(unittest.TestCase): 
     
    2324    configDirEnvVarName = 'NDGSEC_TEST_CONFIG_DIR' 
    2425     
    25     def setUp(self): 
     26    def __init__(self, *arg, **kw): 
    2627        if BaseTestCase.configDirEnvVarName not in os.environ: 
    27             os.environ[BaseTestCase.configDirEnvVarName] = \ 
    28                 join(abspath(dirname(dirname(__file__))), 'config') 
     28            os.environ[BaseTestCase.configDirEnvVarName] = TEST_CONFIG_DIR 
     29                 
     30        unittest.TestCase.__init__(self, *arg, **kw) 
    2931 
    30 mkDataDirPath = lambda file:join(os.environ[BaseTestCase.configDirEnvVarName], 
    31                                  file) 
     32mkDataDirPath = lambda file:join(TEST_CONFIG_DIR, file) 
    3233 
    3334def _getParentDir(depth=0, path=dirname(__file__)): 
  • TI12-security/trunk/python/ndg.security.test/ndg/security/test/unit/openid/relyingparty/validation/test_validation.py

    r5497 r5499  
    1414import os 
    1515import unittest 
    16 from ndg.security.test.unit import BaseTestCase 
     16from ndg.security.test.unit import BaseTestCase, mkDataDirPath 
    1717from ndg.security.server.wsgi.openid.relyingparty.validation import \ 
    18     IdPValidator, IdPValidationDriver, IdPInvalidException 
     18    IdPValidator, IdPValidationDriver, IdPInvalidException, \ 
     19    SSLIdPValidationDriver, SSLClientAuthNValidator 
    1920     
    2021class ProviderWhitelistValidator(IdPValidator): 
     22    """Test stub for Whitelist validator""" 
    2123    def __init__(self): 
    2224        pass 
    2325     
    24     def initialize(self, parameters): 
     26    def initialize(self, **parameters): 
    2527        '''@raise ConfigException:'''  
    26         pass 
    27         
     28        assert('config-file' in parameters) 
     29         
    2830    def validate(self, idpEndpoint, idpIdentity): 
    2931        '''@raise IdPInvalidException: 
     
    3335 
    3436class ProviderIdentifierTestValidator(IdPValidator): 
     37    """Test stub for identifier validator - fixed to reject all IdPs""" 
    3538    def __init__(self): 
    3639        pass 
    3740 
    38     def initialize(self, parameters): 
     41    def initialize(self, **parameters): 
    3942        '''@raise ConfigException:'''  
    40         pass 
     43        assert('config-file' in parameters) 
    4144        
    4245    def validate(self, idpEndpoint, idpIdentity): 
    43         '''@raise IdPInvalidException: 
     46        '''Test method hard wired to raise an invalid IdP exception 
     47        @raise IdPInvalidException: 
    4448        @raise ConfigException:'''  
    45         raise IdPInvalidException() 
     49        raise IdPInvalidException("%s is invalid" % idpEndpoint) 
    4650 
    4751 
     
    5559        return 'myid' 
    5660 
     61from M2Crypto import X509 
    5762 
    58 class IdPValidationTestCase(unittest.TestCase): 
     63class X509StoreCtxPlaceHolder(object): 
     64    x509CertFilePath = mkDataDirPath(os.path.join('pki', 'localhost.crt')) 
     65     
     66    def get_current_cert(self): 
     67        return X509.load_cert(X509StoreCtxPlaceHolder.x509CertFilePath) 
     68     
     69class IdPValidationTestCase(BaseTestCase): 
    5970    thisDir = os.path.dirname(os.path.abspath(__file__)) 
    6071    idpConfigFilePath = os.path.join(thisDir, 'idpvalidator.xml') 
     
    7283        self.assert_(len(validDiscoveries) == 1) 
    7384         
    74  
    75     def test02(self): 
     85    def test02WithIdPConfigFile(self): 
    7686        identifier = IdentifierPlaceHolder() 
    7787        discoveries = [DiscoveryInfoPlaceHolder()] 
     
    8393        self.assert_(len(validDiscoveries) == 1) 
    8494         
     95    def test03SSLValidation(self): 
     96        idpConfigFilePath = os.path.join(IdPValidationTestCase.thisDir,  
     97                                         'ssl-idp-validator.xml') 
     98        idPValidationDriver = SSLIdPValidationDriver( 
     99                                        idpConfigFilePath=idpConfigFilePath) 
     100         
     101        # preVerifyOK set to 1 to indicate all is otherwise OK with  
     102        # verification 
     103        idPValidationDriver(1, X509StoreCtxPlaceHolder()) 
     104         
    85105if __name__ == "__main__": 
    86106    unittest.main()         
Note: See TracChangeset for help on using the changeset viewer.