diff options
author | Eli Collins <elic@assurancetechnologies.com> | 2012-04-10 14:26:52 -0400 |
---|---|---|
committer | Eli Collins <elic@assurancetechnologies.com> | 2012-04-10 14:26:52 -0400 |
commit | 75758ab6138a01ef58d75a0b4cb4c172248d9d8e (patch) | |
tree | 0b3b1db5cb2a2553dca6397231d71493665a3ce5 /passlib | |
parent | d1cd32ca8209513eb7f00201c3555363b3675dd6 (diff) | |
download | passlib-75758ab6138a01ef58d75a0b4cb4c172248d9d8e.tar.gz |
tightened OS crypt backend tests
* split os_crypt tests into separate mixin
* tests now require os_crypt backends to detect some simple incorrect returns from crypt()
- e.g. returning wrong ident prefix, wrong size, etc
- added relevant asserts to all os_crypt backends
* tests now check if platform crypt detection is functioning correctly
via platform_crypt_support dict in tests.
Diffstat (limited to 'passlib')
-rw-r--r-- | passlib/handlers/bcrypt.py | 9 | ||||
-rw-r--r-- | passlib/handlers/des_crypt.py | 9 | ||||
-rw-r--r-- | passlib/handlers/md5_crypt.py | 4 | ||||
-rw-r--r-- | passlib/handlers/sha1_crypt.py | 13 | ||||
-rw-r--r-- | passlib/handlers/sha2_crypt.py | 1 | ||||
-rw-r--r-- | passlib/tests/test_handlers.py | 64 | ||||
-rw-r--r-- | passlib/tests/utils.py | 165 |
7 files changed, 229 insertions, 36 deletions
diff --git a/passlib/handlers/bcrypt.py b/passlib/handlers/bcrypt.py index 8690eb5..c1c4127 100644 --- a/passlib/handlers/bcrypt.py +++ b/passlib/handlers/bcrypt.py @@ -244,8 +244,10 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. "'bcryptor' for bcrypt support" def _calc_checksum_os_crypt(self, secret): - hash = safe_crypt(secret, self._get_config()) + config = self._get_config() + hash = safe_crypt(secret, config) if hash: + assert hash.startswith(config) and len(hash) == len(config)+31 return hash[-31:] else: #NOTE: not checking backends since this is lowest priority, @@ -260,7 +262,9 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. # py3: not supported (patch submitted) if isinstance(secret, unicode): secret = secret.encode("utf-8") - hash = pybcrypt_hashpw(secret, self._get_config()) + config = self._get_config() + hash = pybcrypt_hashpw(secret, config) + assert hash.startswith(config) and len(hash) == len(config)+31 return str_to_uascii(hash[-31:]) def _calc_checksum_bcryptor(self, secret): @@ -281,6 +285,7 @@ class bcrypt(uh.HasManyIdents, uh.HasRounds, uh.HasSalt, uh.HasManyBackends, uh. else: config = self._get_config() hash = bcryptor_engine(False).hash_key(secret, config) + assert hash.startswith(config) and len(hash) == len(config)+31 return str_to_uascii(hash[-31:]) def _calc_checksum_builtin(self, secret): diff --git a/passlib/handlers/des_crypt.py b/passlib/handlers/des_crypt.py index 41e132a..df7683a 100644 --- a/passlib/handlers/des_crypt.py +++ b/passlib/handlers/des_crypt.py @@ -219,7 +219,8 @@ class des_crypt(uh.HasManyBackends, uh.HasSalt, uh.GenericHandler): # no official policy since des-crypt predates unicode hash = safe_crypt(secret, self.salt) if hash: - return hash[2:] + assert hash.startswith(self.salt) and len(hash) == 4 + return hash[-2:] else: return self._calc_checksum_builtin(secret) @@ -331,9 +332,11 @@ class bsdi_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler return raw_ext_crypt(secret, self.rounds, self.salt.encode("ascii")).decode("ascii") def _calc_checksum_os_crypt(self, secret): - hash = safe_crypt(secret, self.to_string()) + config = self.to_string() + hash = safe_crypt(secret, config) if hash: - return hash[9:] + assert hash.startswith(config[:9]) and len(hash) == 20 + return hash[-11:] else: return self._calc_checksum_builtin(secret) diff --git a/passlib/handlers/md5_crypt.py b/passlib/handlers/md5_crypt.py index f55e7c7..ac478b6 100644 --- a/passlib/handlers/md5_crypt.py +++ b/passlib/handlers/md5_crypt.py @@ -266,8 +266,10 @@ class md5_crypt(uh.HasManyBackends, _MD5_Common): return _raw_md5_crypt(secret, self.salt) def _calc_checksum_os_crypt(self, secret): - hash = safe_crypt(secret, self.ident + self.salt) + config = self.ident + self.salt + hash = safe_crypt(secret, config) if hash: + assert hash.startswith(config) and len(hash) == len(config) + 23 return hash[-22:] else: return self._calc_checksum_builtin(secret) diff --git a/passlib/handlers/sha1_crypt.py b/passlib/handlers/sha1_crypt.py index 2bed9e8..35dff46 100644 --- a/passlib/handlers/sha1_crypt.py +++ b/passlib/handlers/sha1_crypt.py @@ -83,8 +83,9 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler rounds, salt, chk = uh.parse_mc3(hash, cls.ident, handler=cls) return cls(rounds=rounds, salt=salt, checksum=chk) - def to_string(self): - return uh.render_mc3(self.ident, self.rounds, self.salt, self.checksum) + def to_string(self, config=False): + chk = None if config else self.checksum + return uh.render_mc3(self.ident, self.rounds, self.salt, chk) #========================================================= #backend @@ -95,7 +96,7 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler @classproperty def _has_backend_os_crypt(cls): - return test_crypt("test", '$sha1$1$Wq3GL2Vp$C8U''25GvfHS8qGHim' + return test_crypt("test", '$sha1$1$Wq3GL2Vp$C8U25GvfHS8qGHim' 'ExLaiSFlGkAe') def _calc_checksum_builtin(self, secret): @@ -122,9 +123,11 @@ class sha1_crypt(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, uh.GenericHandler ] def _calc_checksum_os_crypt(self, secret): - hash = safe_crypt(secret, self.to_string()) + config = self.to_string(config=True) + hash = safe_crypt(secret, config) if hash: - return hash[hash.rindex("$")+1:] + assert hash.startswith(config) and len(hash) == len(config) + 29 + return hash[-28:] else: return self._calc_checksum_builtin(secret) diff --git a/passlib/handlers/sha2_crypt.py b/passlib/handlers/sha2_crypt.py index dc68226..fccc4e1 100644 --- a/passlib/handlers/sha2_crypt.py +++ b/passlib/handlers/sha2_crypt.py @@ -349,6 +349,7 @@ class _SHA2_Common(uh.HasManyBackends, uh.HasRounds, uh.HasSalt, # NOTE: avoiding full parsing routine via from_string().checksum, # and just extracting the bit we need. cs = self.checksum_size + assert hash.startswith(self.ident) and hash[-cs-1] == _UDOLLAR return hash[-cs:] else: return self._calc_checksum_builtin(secret) diff --git a/passlib/tests/test_handlers.py b/passlib/tests/test_handlers.py index 6584f9e..8a119a2 100644 --- a/passlib/tests/test_handlers.py +++ b/passlib/tests/test_handlers.py @@ -166,6 +166,14 @@ class _bcrypt_test(HandlerCase): # but we can reliably correct & issue a warning for that. ] + platform_crypt_support = dict( + freebsd=True, + openbsd=True, + netbsd=True, + # linux - some systems + # solaris - depends on policy + ) + #=============================================================== # override some methods #=============================================================== @@ -412,6 +420,15 @@ class _bsdi_crypt_test(HandlerCase): "_K1.!crsmZxOLzfJH8iw" ] + platform_crypt_support = dict( + freebsd=True, + openbsd=False, + netbsd=True, + linux=False, + solaris=False, + # darwin ? + ) + os_crypt_bsdi_crypt_test = create_backend_case(_bsdi_crypt_test, "os_crypt") builtin_bsdi_crypt_test = create_backend_case(_bsdi_crypt_test, "builtin") @@ -589,6 +606,15 @@ class _des_crypt_test(HandlerCase): '!gAwTx2l6NADI', ] + platform_crypt_support = dict( + freebsd=True, + openbsd=True, + netbsd=True, + linux=True, + solaris=True, + # darwin? + ) + def test_90_invalid_secret_chars(self): self.assertRaises(ValueError, self.do_encrypt, 'sec\x00t') @@ -1057,6 +1083,15 @@ class _md5_crypt_test(HandlerCase): '$1$dOHYPKoP$tnxS1T8Q6VVn3kpV8cN6o!', ] + platform_crypt_support = dict( + freebsd=True, + openbsd=True, + netbsd=True, + linux=True, + solaris=True, + # darwin? + ) + os_crypt_md5_crypt_test = create_backend_case(_md5_crypt_test, "os_crypt") builtin_md5_crypt_test = create_backend_case(_md5_crypt_test, "builtin") @@ -1951,6 +1986,15 @@ class _sha1_crypt_test(HandlerCase): '$sha1$01773$uV7PTeux$I9oHnvwPZHMO0Nq6/WgyGV/tDJIH', ] + platform_crypt_support = dict( + freebsd=False, + openbsd=False, + netbsd=True, + linux=False, + solaris=False, + darwin=False, + ) + os_crypt_sha1_crypt_test = create_backend_case(_sha1_crypt_test, "os_crypt") builtin_sha1_crypt_test = create_backend_case(_sha1_crypt_test, "builtin") @@ -2079,6 +2123,15 @@ class _sha256_crypt_test(HandlerCase): filter_config_warnings = True # rounds too low, salt too small + platform_crypt_support = dict( + freebsd=False, + openbsd=False, + netbsd=False, + linux=True, + # solaris - depends on policy + # darwin ?, + ) + os_crypt_sha256_crypt_test = create_backend_case(_sha256_crypt_test, "os_crypt") builtin_sha256_crypt_test = create_backend_case(_sha256_crypt_test, "builtin") @@ -2158,6 +2211,8 @@ class _sha512_crypt_test(HandlerCase): filter_config_warnings = True # rounds too low, salt too small + platform_crypt_support = _sha256_crypt_test.platform_crypt_support + os_crypt_sha512_crypt_test = create_backend_case(_sha512_crypt_test, "os_crypt") builtin_sha512_crypt_test = create_backend_case(_sha512_crypt_test, "builtin") @@ -2259,6 +2314,15 @@ class sun_md5_crypt_test(HandlerCase): ] + platform_crypt_support = dict( + freebsd=False, + openbsd=False, + netbsd=False, + linux=False, + solaris=True, + darwin=False, + ) + def do_verify(self, secret, hash): # override to fake error for "$..." hash strings listed in known_config. # these have to be hash strings, in order to test bare salt issue. diff --git a/passlib/tests/utils.py b/passlib/tests/utils.py index 7c79975..6dedc3d 100644 --- a/passlib/tests/utils.py +++ b/passlib/tests/utils.py @@ -698,31 +698,9 @@ class HandlerCase(TestCase): if backend: if not hasattr(handler, "set_backend"): raise RuntimeError("handler doesn't support multiple backends") - if backend == "os_crypt" and not handler.has_backend("os_crypt"): - self._patch_safe_crypt() self.addCleanup(handler.set_backend, handler.get_backend()) handler.set_backend(backend) - def _patch_safe_crypt(self): - """if crypt() doesn't support current hash alg, this patches - safe_crypt() so that it transparently uses another one of the handler's - backends, so that we can go ahead and test as much of code path - as possible. - """ - handler = self.handler - alt_backend = _find_alternate_backend(handler, "os_crypt") - if not alt_backend: - raise AssertionError("handler has no available backends!") - import passlib.utils as mod - def crypt_stub(secret, hash): - with temporary_backend(handler, alt_backend): - hash = handler.genhash(secret, hash) - assert isinstance(hash, str) - return hash - self.addCleanup(setattr, mod, "_crypt", mod._crypt) - mod._crypt = crypt_stub - self.using_patched_crypt = True - #========================================================= # basic tests #========================================================= @@ -1635,6 +1613,115 @@ class HandlerCase(TestCase): # eoc #========================================================= +class OsCryptMixin(HandlerCase): + """helper used by create_backend_case() which adds additional features + to test the os_crypt backend. + + * if crypt support is missing, inserts fake crypt support to simulate + a working safe_crypt, to test passlib's codepath as fully as possible. + + * extra tests to verify non-conformant crypt implementations are handled + correctly. + + * check that native crypt support is detected correctly for known platforms. + """ + #========================================================= + # option flags + #========================================================= + # platforms that are known to support / not support this hash natively. + # encodeds as os.platform prefixes. + platform_crypt_support = dict() + + # TODO: test that os_crypt support is detected correct on the expected + # platofrms. + + #========================================================= + # instance attrs + #========================================================= + __unittest_skip = True + + # force this backend + backend = "os_crypt" + + # flag read by HandlerCase to detect if fake os crypt is enabled. + using_patched_crypt = False + + #========================================================= + # setup + #========================================================= + def setUp(self): + assert self.backend == "os_crypt" + if not self.handler.has_backend("os_crypt"): + self.handler.get_backend() # hack to prevent recursion issue + self._patch_safe_crypt() + HandlerCase.setUp(self) + + def _patch_safe_crypt(self): + """if crypt() doesn't support current hash alg, this patches + safe_crypt() so that it transparently uses another one of the handler's + backends, so that we can go ahead and test as much of code path + as possible. + """ + handler = self.handler + alt_backend = _find_alternate_backend(handler, "os_crypt") + if not alt_backend: + raise AssertionError("handler has no available backends!") + import passlib.utils as mod + def crypt_stub(secret, hash): + with temporary_backend(handler, alt_backend): + hash = handler.genhash(secret, hash) + assert isinstance(hash, str) + return hash + self.addCleanup(setattr, mod, "_crypt", mod._crypt) + mod._crypt = crypt_stub + self.using_patched_crypt = True + + #========================================================= + # custom tests + #========================================================= + def test_80_faulty_crypt(self): + "test with faulty crypt()" + # patch safe_crypt to return mock value. + import passlib.utils as mod + self.addCleanup(setattr, mod, "_crypt", mod._crypt) + crypt_value = [None] + mod._crypt = lambda secret, config: crypt_value[0] + + # prepare framework + config = self.do_genconfig() + hash = self.get_sample_hash()[1] + exc_types = (AssertionError,) + + def test(value): + crypt_value[0] = value + self.assertRaises(exc_types, self.do_genhash, "stub", config) + self.assertRaises(exc_types, self.do_encrypt, "stub") + self.assertRaises(exc_types, self.do_verify, "stub", hash) + + test('$x' + hash[2:]) # detect wrong prefix + test(hash[:-1]) # detect too short + test(hash + 'x') # detect too long + + def test_81_crypt_support(self): + "test crypt support detection" + platform = sys.platform + for name, flag in self.platform_crypt_support.items(): + if not platform.startswith(name): + continue + if flag != self.using_patched_crypt: + return + if flag: + self.fail("expected %r platform would have native support " + "for %r" % (platform, self.handler.name)) + else: + self.fail("expected %r platform would NOT have native support " + "for %r" % (platform, self.handler.name)) + raise self.skipTest("no data for %r platform" % platform) + + #========================================================= + # eoc + #========================================================= + class UserHandlerMixin(HandlerCase): """helper for handlers w/ 'user' context kwd; mixin for HandlerCase @@ -1643,11 +1730,21 @@ class UserHandlerMixin(HandlerCase): calls. as well, passing in a pair of strings as the password will be interpreted as (secret,user) """ - __unittest_skip = True + #========================================================= + # option flags + #========================================================= default_user = "user" requires_user = True user_case_insensitive = False + #========================================================= + # instance attrs + #========================================================= + __unittest_skip = True + + #========================================================= + # custom tests + #========================================================= def test_80_user(self): "test user context keyword" handler = self.handler @@ -1687,6 +1784,10 @@ class UserHandlerMixin(HandlerCase): # TODO: user size? kinda dicey, depends on algorithm. + #========================================================= + # override test helpers + #========================================================= + def is_secret_8bit(self, secret): secret = self._insert_user({}, secret) return not is_ascii_safe(secret) @@ -1715,13 +1816,22 @@ class UserHandlerMixin(HandlerCase): secret = self._insert_user(kwds, secret) return self.handler.genhash(secret, config, **kwds) + #========================================================= + # modify fuzz testing + #========================================================= fuzz_user_alphabet = u("asdQWE123") + fuzz_settings = HandlerCase.fuzz_settings + ["user"] + def get_fuzz_user(self): if not self.requires_user and rng.random() < .1: return None return getrandstr(rng, self.fuzz_user_alphabet, rng.randint(2,10)) + #========================================================= + # eoc + #========================================================= + #========================================================= #backend test helpers #========================================================= @@ -1805,10 +1915,15 @@ def create_backend_case(base_class, backend, module=None): if not enable and ut_version < 2: return None - #create subclass of 'base' which uses correct backend + # pick bases + bases = (base_class,) + if backend == "os_crypt": + bases += (OsCryptMixin,) + + # create subclass to test backend backend_class = type( "%s_%s" % (backend, handler.name), - (base_class,), + bases, dict( descriptionPrefix = "%s (%s backend)" % (handler.name, backend), backend = backend, |