diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py index 73753fe6c4d710af5e633c6ee683c3f195a034f0..bb89df0e0e07e5731120c73d41251aba54a8a98b 100644 --- a/src/_zkapauthorizer/_storage_server.py +++ b/src/_zkapauthorizer/_storage_server.py @@ -88,6 +88,7 @@ from .foolscap import ( RIPrivacyPassAuthorizedStorageServer, ) from .storage_common import ( + MorePassesRequired, pass_value_attribute, required_passes, allocate_buckets_message, @@ -102,26 +103,6 @@ from .storage_common import ( SLOT_HEADER_SIZE = 468 LEASE_TRAILER_SIZE = 4 -@attr.s -class MorePassesRequired(Exception): - """ - Storage operations fail with ``MorePassesRequired`` when they are not - accompanied by a sufficient number of valid passes. - - :ivar int valid_count: The number of valid passes presented in the - operation. - - ivar int required_count: The number of valid passes which must be - presented for the operation to be authorized. - - :ivar list[int] signature_check_failed: Indices into the supplied list of - passes indicating passes which failed the signature check. - """ - valid_count = attr.ib() - required_count = attr.ib() - signature_check_failed = attr.ib() - - @attr.s class _ValidationResult(object): """ diff --git a/src/_zkapauthorizer/api.py b/src/_zkapauthorizer/api.py index 365e39a23026e1b7692267d12c895f45cfaeda64..70e4725e5c89d503e6973e2b708cecede48bcd66 100644 --- a/src/_zkapauthorizer/api.py +++ b/src/_zkapauthorizer/api.py @@ -20,8 +20,10 @@ __all__ = [ "ZKAPAuthorizer", ] -from ._storage_server import ( +from .storage_common import ( MorePassesRequired, +) +from ._storage_server import ( LeaseRenewalRequired, ZKAPAuthorizerStorageServer, ) diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py index f00997b1c6db9b240eebd7dade935c5c6dc8e917..1a52b4d26b855baefe3802b3203283fb0a7306f6 100644 --- a/src/_zkapauthorizer/storage_common.py +++ b/src/_zkapauthorizer/storage_common.py @@ -30,6 +30,26 @@ from .validators import ( greater_than, ) +@attr.s +class MorePassesRequired(Exception): + """ + Storage operations fail with ``MorePassesRequired`` when they are not + accompanied by a sufficient number of valid passes. + + :ivar int valid_count: The number of valid passes presented in the + operation. + + ivar int required_count: The number of valid passes which must be + presented for the operation to be authorized. + + :ivar list[int] signature_check_failed: Indices into the supplied list of + passes indicating passes which failed the signature check. + """ + valid_count = attr.ib() + required_count = attr.ib() + signature_check_failed = attr.ib() + + def _message_maker(label): def make_message(storage_index): return u"{label} {storage_index}".format( diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py index a267c0667f3fe1b3101ab9e5a7a9eb10d8091a32..612a8538f5704e5cf68a135e5f17be3cad3769d7 100644 --- a/src/_zkapauthorizer/tests/test_storage_protocol.py +++ b/src/_zkapauthorizer/tests/test_storage_protocol.py @@ -111,6 +111,7 @@ from .foolscap import ( LocalRemote, ) from ..api import ( + MorePassesRequired, ZKAPAuthorizerStorageServer, ZKAPAuthorizerStorageClient, ) @@ -215,6 +216,55 @@ class ShareTests(TestCase): succeeded(matches_version_dictionary()), ) + @given( + storage_index=storage_indexes(), + renew_secret=lease_renew_secrets(), + cancel_secret=lease_cancel_secrets(), + sharenums=sharenum_sets(), + size=sizes(), + ) + def test_rejected_passes_reported(self, storage_index, renew_secret, cancel_secret, sharenums, size): + """ + Any passes rejected by the storage server are reported with a + ``MorePassesRequired`` exception sent to the client. + """ + # Hypothesis causes our storage server to be used many times. Clean + # up between iterations. + cleanup_storage_server(self.anonymous_storage_server) + + # Break our infinite pass factory by replacing the expected key with a + # new one. Now the passes are mis-signed as far as the server is + # concerned. The clunky way we control pass generation means it's + # hard to have anything but an all-or-nothing test. Perhaps some + # future refactoring will let us exercise a mix of passes with valid + # and invalid signatures. + self.signing_key = random_signing_key() + + num_passes = required_passes(self.pass_value, [size] * len(sharenums)) + + self.assertThat( + self.client.allocate_buckets( + storage_index, + renew_secret, + cancel_secret, + sharenums, + size, + canary=self.canary, + ), + failed( + AfterPreprocessing( + lambda f: f.value, + Equals( + MorePassesRequired( + valid_count=0, + required_count=num_passes, + signature_check_failed=range(num_passes), + ), + ), + ), + ), + ) + @given( storage_index=storage_indexes(), renew_secret=lease_renew_secrets(),