diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index cf88d3c48de1e9306be711f9260465db41508e44..b3e672fe52b76bc335481ad7adfe4e76d39cb974 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -35,6 +35,13 @@ For example:: Note that ``ristretto-issuer-root-url`` must agree with whichever storage servers the client will be configured to interact with. If the values are not the same, the client will decline to use the storage servers. +The client can also be configured with the value of a single pass:: + + [storageclient.plugins.privatestorageio-zkapauthz-v1] + pass-value = 1048576 + +The value given here must agree with the value servers use in their configuration or the storage service will be unusable. + Server ------ @@ -49,6 +56,14 @@ Then also configure the Ristretto-flavored PrivacyPass issuer the server will an [storageserver.plugins.privatestorageio-zkapauthz-v1] ristretto-issuer-root-url = https://issuer.example.invalid/ +The value of a single pass in the system can be configured here as well:: + + [storageserver.plugins.privatestorageio-zkapauthz-v1] + pass-value = 1048576 + +If no ``pass-value`` is given then a default will be used. +The value given here must agree with the value clients use in their configuration or the storage service will be unusable. + The storage server must also be configured with the path to the Ristretto-flavored PrivacyPass signing key. To avoid placing secret material in tahoe.cfg, this configuration is done using a path:: diff --git a/setup.cfg b/setup.cfg index 51670eea1a8b93fa39c6191b37715f9b166e1c18..371a19b343c204fb576b2c6d8c77d969160d40e3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ packages = install_requires = attrs zope.interface + eliot aniso8601 python-challenge-bypass-ristretto # Inherit our Twisted dependency from tahoe-lafs so we don't accidentally diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index 2ed966d8af44e357e789feb8bfc38bb2b92ef4c4..ee57951537cb0800d74412e9e8579f0a7ac279df 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -45,6 +45,11 @@ from twisted.internet.defer import ( succeed, ) +from eliot import ( + MessageType, + Field, +) + from allmydata.interfaces import ( IFoolscapStoragePlugin, IAnnounceableStorageServer, @@ -71,7 +76,10 @@ from .model import ( from .resource import ( from_configuration as resource_from_configuration, ) - +from .storage_common import ( + BYTES_PER_PASS, + get_configured_pass_value, +) from .controller import ( get_redeemer, ) @@ -83,6 +91,24 @@ from .lease_maintenance import ( _log = Logger() +PRIVACYPASS_MESSAGE = Field( + u"message", + unicode, + u"The PrivacyPass request-binding data associated with a pass.", +) + +PASS_COUNT = Field( + u"count", + int, + u"A number of passes.", +) + +GET_PASSES = MessageType( + u"zkapauthorizer:get-passes", + [PRIVACYPASS_MESSAGE, PASS_COUNT], + u"Passes are being spent.", +) + @implementer(IAnnounceableStorageServer) @attr.s class AnnounceableStorageServer(object): @@ -134,6 +160,7 @@ class ZKAPAuthorizer(object): def get_storage_server(self, configuration, get_anonymous_storage_server): kwargs = configuration.copy() root_url = kwargs.pop(u"ristretto-issuer-root-url") + pass_value = kwargs.pop(u"pass-value", BYTES_PER_PASS) signing_key = SigningKey.decode_base64( FilePath( kwargs.pop(u"ristretto-signing-key-path"), @@ -144,7 +171,8 @@ class ZKAPAuthorizer(object): } storage_server = ZKAPAuthorizerStorageServer( get_anonymous_storage_server(), - signing_key, + pass_value=pass_value, + signing_key=signing_key, **kwargs ) return succeed( @@ -167,9 +195,15 @@ class ZKAPAuthorizer(object): extract_unblinded_tokens = self._get_store(node_config).extract_unblinded_tokens def get_passes(message, count): unblinded_tokens = extract_unblinded_tokens(count) - return redeemer.tokens_to_passes(message, unblinded_tokens) + passes = redeemer.tokens_to_passes(message, unblinded_tokens) + GET_PASSES.log( + message=message, + count=count, + ) + return passes return ZKAPAuthorizerStorageClient( + get_configured_pass_value(node_config), get_rref, get_passes, ) diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py index a51fbfd1402c74e0f95ebddbd325e99f7ea076f4..6559b732e6a1bcd67396dea3d561162f2ce31c5c 100644 --- a/src/_zkapauthorizer/_storage_client.py +++ b/src/_zkapauthorizer/_storage_client.py @@ -34,7 +34,7 @@ from allmydata.interfaces import ( ) from .storage_common import ( - BYTES_PER_PASS, + pass_value_attribute, required_passes, allocate_buckets_message, add_lease_message, @@ -92,7 +92,7 @@ class ZKAPAuthorizerStorageClient(object): _expected_remote_interface_name = ( "RIPrivacyPassAuthorizedStorageServer.tahoe.privatestorage.io" ) - + _pass_value = pass_value_attribute() _get_rref = attr.ib() _get_passes = attr.ib() @@ -148,7 +148,7 @@ class ZKAPAuthorizerStorageClient(object): "allocate_buckets", self._get_encoded_passes( allocate_buckets_message(storage_index), - required_passes(BYTES_PER_PASS, [allocated_size] * len(sharenums)), + required_passes(self._pass_value, [allocated_size] * len(sharenums)), ), storage_index, renew_secret, @@ -179,7 +179,7 @@ class ZKAPAuthorizerStorageClient(object): storage_index, None, )).values() - num_passes = required_passes(BYTES_PER_PASS, share_sizes) + num_passes = required_passes(self._pass_value, share_sizes) # print("Adding lease to {!r} with sizes {} with {} passes".format( # storage_index, # share_sizes, @@ -206,7 +206,7 @@ class ZKAPAuthorizerStorageClient(object): storage_index, None, )).values() - num_passes = required_passes(BYTES_PER_PASS, share_sizes) + num_passes = required_passes(self._pass_value, share_sizes) returnValue(( yield self._rref.callRemote( "renew_lease", @@ -265,6 +265,7 @@ class ZKAPAuthorizerStorageClient(object): ) # Determine the cost of the new storage for the operation. required_new_passes = get_required_new_passes_for_mutable_write( + self._pass_value, current_sizes, tw_vectors, ) diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py index d8c747b451bd1b95ed841ace69094b92e6b2df53..a2d4b9f2c5cce038eff98184ae4859cbde8f8b81 100644 --- a/src/_zkapauthorizer/_storage_server.py +++ b/src/_zkapauthorizer/_storage_server.py @@ -86,7 +86,7 @@ from .foolscap import ( RIPrivacyPassAuthorizedStorageServer, ) from .storage_common import ( - BYTES_PER_PASS, + pass_value_attribute, required_passes, allocate_buckets_message, add_lease_message, @@ -153,6 +153,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): LEASE_PERIOD = timedelta(days=31) _original = attr.ib(validator=provides(RIStorageServer)) + _pass_value = pass_value_attribute() _signing_key = attr.ib(validator=instance_of(SigningKey)) _clock = attr.ib( validator=provides(IReactorTime), @@ -217,7 +218,12 @@ class ZKAPAuthorizerStorageServer(Referenceable): allocate_buckets_message(storage_index), passes, ) - check_pass_quantity_for_write(len(valid_passes), sharenums, allocated_size) + check_pass_quantity_for_write( + self._pass_value, + len(valid_passes), + sharenums, + allocated_size, + ) return self._original.remote_allocate_buckets( storage_index, @@ -243,6 +249,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): # print("server add_lease({}, {!r})".format(len(passes), storage_index)) valid_passes = self._validate_passes(add_lease_message(storage_index), passes) check_pass_quantity_for_lease( + self._pass_value, storage_index, valid_passes, self._original, @@ -256,6 +263,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): """ valid_passes = self._validate_passes(renew_lease_message(storage_index), passes) check_pass_quantity_for_lease( + self._pass_value, storage_index, valid_passes, self._original, @@ -324,6 +332,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): renew_leases = True required_new_passes = get_required_new_passes_for_mutable_write( + self._pass_value, current_sizes, tw_vectors, ) @@ -372,23 +381,7 @@ def has_active_lease(storage_server, storage_index, now): ) -def check_pass_quantity_for_lease(storage_index, valid_passes, storage_server): - """ - Check that the given number of passes is sufficient to add or renew a - lease for one period for the given storage index. - """ - allocated_sizes = dict( - get_share_sizes( - storage_server, - storage_index, - list(get_all_share_numbers(storage_server, storage_index)), - ), - ).values() - # print("allocated_sizes: {}".format(allocated_sizes)) - check_pass_quantity(len(valid_passes), allocated_sizes) - # print("Checked out") - -def check_pass_quantity(valid_count, share_sizes): +def check_pass_quantity(pass_value, valid_count, share_sizes): """ Check that the given number of passes is sufficient to cover leases for one period for shares of the given sizes. @@ -402,14 +395,30 @@ def check_pass_quantity(valid_count, share_sizes): :return: ``None`` if the given number of passes is sufficient. """ - required_pass_count = required_passes(BYTES_PER_PASS, share_sizes) + required_pass_count = required_passes(pass_value, share_sizes) if valid_count < required_pass_count: raise MorePassesRequired( valid_count, required_pass_count, ) -def check_pass_quantity_for_write(valid_count, sharenums, allocated_size): + +def check_pass_quantity_for_lease(pass_value, storage_index, valid_passes, storage_server): + """ + Check that the given number of passes is sufficient to add or renew a + lease for one period for the given storage index. + """ + allocated_sizes = dict( + get_share_sizes( + storage_server, + storage_index, + list(get_all_share_numbers(storage_server, storage_index)), + ), + ).values() + check_pass_quantity(pass_value, len(valid_passes), allocated_sizes) + + +def check_pass_quantity_for_write(pass_value, valid_count, sharenums, allocated_size): """ Determine if the given number of valid passes is sufficient for an attempted write. @@ -423,7 +432,7 @@ def check_pass_quantity_for_write(valid_count, sharenums, allocated_size): :return: ``None`` if the number of valid passes given is sufficient. """ - check_pass_quantity(valid_count, [allocated_size] * len(sharenums)) + check_pass_quantity(pass_value, valid_count, [allocated_size] * len(sharenums)) def get_all_share_paths(storage_server, storage_index): diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index 8eb85ee47f3a645f110cd3d5de297a5576303c2f..baff206c28c14931f9d403d47b4938cccb756b10 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -27,9 +27,6 @@ from json import ( from datetime import ( datetime, ) -from base64 import ( - b64decode, -) from zope.interface import ( Interface, implementer, @@ -56,8 +53,15 @@ from ._base64 import ( urlsafe_b64decode, ) +from .validators import ( + is_base64_encoded, + has_length, + greater_than, +) + from .storage_common import ( - BYTES_PER_PASS, + pass_value_attribute, + get_configured_pass_value, required_passes, ) @@ -171,6 +175,8 @@ class VoucherStore(object): """ _log = Logger() + pass_value = pass_value_attribute() + database_path = attr.ib(validator=attr.validators.instance_of(FilePath)) now = attr.ib() @@ -196,6 +202,7 @@ class VoucherStore(object): connect=connect, ) return cls( + get_configured_pass_value(node_config), db_path, now, conn, @@ -504,7 +511,7 @@ class VoucherStore(object): :return LeaseMaintenance: A new, started lease maintenance object. """ - m = LeaseMaintenance(self.now, self._connection) + m = LeaseMaintenance(self.pass_value, self.now, self._connection) m.start() return m @@ -548,6 +555,8 @@ class LeaseMaintenance(object): the ``observe`` and ``finish`` methods to persist state about a lease maintenance run. + :ivar int _pass_value: The value of a single ZKAP in byte-months. + :ivar _now: A no-argument callable which returns a datetime giving a time to use as current. @@ -558,6 +567,7 @@ class LeaseMaintenance(object): objects, the database row id that corresponds to the started run. This is used to make sure future updates go to the right row. """ + _pass_value = pass_value_attribute() _now = attr.ib() _connection = attr.ib() _rowid = attr.ib(default=None) @@ -584,7 +594,7 @@ class LeaseMaintenance(object): """ Record a storage shares of the given sizes. """ - count = required_passes(BYTES_PER_PASS, sizes) + count = required_passes(self._pass_value, sizes) cursor.execute( """ UPDATE [lease-maintenance-spending] @@ -627,46 +637,6 @@ class LeaseMaintenanceActivity(object): # x = store.get_latest_lease_maintenance_activity() # xs.started, xs.passes_required, xs.finished -def is_base64_encoded(b64decode=b64decode): - def validate_is_base64_encoded(inst, attr, value): - try: - b64decode(value.encode("ascii")) - except (TypeError, Error): - raise TypeError( - "{name!r} must be base64 encoded unicode, (got {value!r})".format( - name=attr.name, - value=value, - ), - ) - return validate_is_base64_encoded - -def has_length(expected): - def validate_has_length(inst, attr, value): - if len(value) != expected: - raise ValueError( - "{name!r} must have length {expected}, instead has length {actual}".format( - name=attr.name, - expected=expected, - actual=len(value), - ), - ) - return validate_has_length - -def greater_than(expected): - def validate_relation(inst, attr, value): - if value > expected: - return None - - raise ValueError( - "{name!r} must be greater than {expected}, instead it was {actual}".format( - name=attr.name, - expected=expected, - actual=value, - ), - ) - return validate_relation - - @attr.s(frozen=True) class UnblindedToken(object): """ diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py index 9bf9435e69e5429cf7bdf596d7e1b18fe0472da1..f00997b1c6db9b240eebd7dade935c5c6dc8e917 100644 --- a/src/_zkapauthorizer/storage_common.py +++ b/src/_zkapauthorizer/storage_common.py @@ -24,6 +24,12 @@ from base64 import ( b64encode, ) +import attr + +from .validators import ( + greater_than, +) + def _message_maker(label): def make_message(storage_index): return u"{label} {storage_index}".format( @@ -41,7 +47,22 @@ slot_testv_and_readv_and_writev_message = _message_maker(u"slot_testv_and_readv_ # The number of bytes we're willing to store for a lease period for each pass # submitted. -BYTES_PER_PASS = 128 * 1024 +BYTES_PER_PASS = 1024 * 1024 + +def get_configured_pass_value(node_config): + """ + Determine the configuration-specified value of a single ZKAP. + + If no value is explicitly configured, a default value is returned. The + value is read from the **pass-value** option of the ZKAPAuthorizer plugin + client section. + """ + section_name = u"storageclient.plugins.privatestorageio-zkapauthz-v1" + return int(node_config.get_config( + section=section_name, + option=u"pass-value", + default=BYTES_PER_PASS, + )) def required_passes(bytes_per_pass, share_sizes): """ @@ -136,10 +157,13 @@ def get_implied_data_length(data_vector, new_length): return min(new_length, data_based_size) -def get_required_new_passes_for_mutable_write(current_sizes, tw_vectors): +def get_required_new_passes_for_mutable_write(pass_value, current_sizes, tw_vectors): + """ + :param int pass_value: The value of a single pass in byte-months. + """ # print("get_required_new_passes_for_mutable_write({}, {})".format(current_sizes, summarize(tw_vectors))) current_passes = required_passes( - BYTES_PER_PASS, + pass_value, current_sizes.values(), ) @@ -155,7 +179,7 @@ def get_required_new_passes_for_mutable_write(current_sizes, tw_vectors): new_sizes.update() new_passes = required_passes( - BYTES_PER_PASS, + pass_value, new_sizes.values(), ) required_new_passes = new_passes - current_passes @@ -180,3 +204,14 @@ def summarize(tw_vectors): for (sharenum, (test_vector, data_vectors, new_length)) in tw_vectors.items() } + +def pass_value_attribute(): + """ + Define an attribute for an attrs-based object which can hold a pass value. + """ + return attr.ib( + validator=attr.validators.and_( + attr.validators.instance_of((int, long)), + greater_than(0), + ), + ) diff --git a/src/_zkapauthorizer/tests/eliot.py b/src/_zkapauthorizer/tests/eliot.py new file mode 100644 index 0000000000000000000000000000000000000000..710737d948cc4e069d12c265277e95dee569e133 --- /dev/null +++ b/src/_zkapauthorizer/tests/eliot.py @@ -0,0 +1,90 @@ +# Copyright 2019 PrivateStorage.io, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Eliot testing helpers. +""" + +from __future__ import ( + absolute_import, +) + +from functools import ( + wraps, +) + +from unittest import ( + SkipTest, +) + +from eliot import ( + MemoryLogger, +) + +from eliot.testing import ( + swap_logger, + check_for_errors, +) + +# validate_logging and capture_logging copied from Eliot around 1.11. We +# can't upgrade past 1.7 because we're not Python 3 compatible. +def validate_logging(assertion, *assertionArgs, **assertionKwargs): + def decorator(function): + @wraps(function) + def wrapper(self, *args, **kwargs): + skipped = False + + kwargs["logger"] = logger = MemoryLogger() + self.addCleanup(check_for_errors, logger) + # TestCase runs cleanups in reverse order, and we want this to + # run *before* tracebacks are checked: + if assertion is not None: + self.addCleanup( + lambda: skipped + or assertion(self, logger, *assertionArgs, **assertionKwargs) + ) + try: + return function(self, *args, **kwargs) + except SkipTest: + skipped = True + raise + + return wrapper + + return decorator + + +def capture_logging(assertion, *assertionArgs, **assertionKwargs): + """ + Capture and validate all logging that doesn't specify a L{Logger}. + + See L{validate_logging} for details on the rest of its behavior. + """ + + def decorator(function): + @validate_logging(assertion, *assertionArgs, **assertionKwargs) + @wraps(function) + def wrapper(self, *args, **kwargs): + logger = kwargs["logger"] + previous_logger = swap_logger(logger) + + def cleanup(): + swap_logger(previous_logger) + + self.addCleanup(cleanup) + return function(self, *args, **kwargs) + + return wrapper + + return decorator diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 48ca9b960f1af29df554136543dd076461960b35..a6a96f9c82a4b9615a58b11a68445c094a92506a 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -135,7 +135,6 @@ from ..resource import ( ) from ..storage_common import ( - BYTES_PER_PASS, required_passes, ) @@ -578,7 +577,7 @@ class UnblindedTokenTests(TestCase): total = 0 activity = root.store.start_lease_maintenance() for sizes in size_observations: - total += required_passes(BYTES_PER_PASS, sizes) + total += required_passes(root.store.pass_value, sizes) activity.observe(sizes) activity.finish() diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 0e5ebd3e971de668d3ca3de36e356d943d62531f..5bd3e3145d12b90a67ef27ce4aaa7dc382864ef4 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -69,10 +69,6 @@ from twisted.python.runtime import ( platform, ) -from ..storage_common import ( - BYTES_PER_PASS, -) - from ..model import ( StoreOpenError, NotEnoughTokens, @@ -391,8 +387,11 @@ class LeaseMaintenanceTests(TestCase): tuples( # The activity itself, in pass count integers(min_value=1, max_value=2 ** 16 - 1), - # Amount by which to trim back the share sizes - integers(min_value=0, max_value=BYTES_PER_PASS - 1), + # Amount by which to trim back the share sizes. This + # might exceed the value of a single pass but we don't + # know that value yet. We'll map it into a coherent + # range with mod inside the test. + integers(min_value=0), ), ), # How much time passes before this activity finishes @@ -418,8 +417,9 @@ class LeaseMaintenanceTests(TestCase): passes_required = 0 for (num_passes, trim_size) in sizes: passes_required += num_passes + trim_size %= store.pass_value x.observe([ - num_passes * BYTES_PER_PASS - trim_size, + num_passes * store.pass_value - trim_size, ]) now += finish_delay x.finish() diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index 18dcebc3ddaef76e26fbd04386acda5d66503bbe..c45efafb5bf54a9f97a804d0e0a9399f454e69c0 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -41,7 +41,12 @@ from testtools import ( from testtools.matchers import ( Always, Contains, + Equals, AfterPreprocessing, + MatchesAll, + HasLength, + AllMatch, + ContainsDict, ) from testtools.twistedsupport import ( succeeded, @@ -79,6 +84,10 @@ from allmydata.client import ( create_client_from_config, ) +from eliot.testing import ( + LoggedMessage, +) + from twisted.python.filepath import ( FilePath, ) @@ -95,6 +104,10 @@ from twisted.plugins.zkapauthorizer import ( storage_server, ) +from .._plugin import ( + GET_PASSES, +) + from ..foolscap import ( RIPrivacyPassAuthorizedStorageServer, ) @@ -108,8 +121,8 @@ from ..controller import ( DummyRedeemer, ) from ..storage_common import ( - BYTES_PER_PASS, required_passes, + allocate_buckets_message, ) from .._storage_client import ( IncorrectStorageServerReference, @@ -143,6 +156,11 @@ from .foolscap import ( DummyReferenceable, ) +from .eliot import ( + capture_logging, +) + + SIGNING_KEY_PATH = FilePath(__file__).sibling(u"testing-signing.key") @@ -386,18 +404,20 @@ class ClientPluginTests(TestCase): ) @given( - tahoe_configs_with_dummy_redeemer, - datetimes(), - announcements(), - vouchers(), - storage_indexes(), - lease_renew_secrets(), - lease_cancel_secrets(), - sharenum_sets(), - sizes(), + get_config=tahoe_configs_with_dummy_redeemer, + now=datetimes(), + announcement=announcements(), + voucher=vouchers(), + storage_index=storage_indexes(), + renew_secret=lease_renew_secrets(), + cancel_secret=lease_cancel_secrets(), + sharenums=sharenum_sets(), + size=sizes(), ) + @capture_logging(lambda self, logger: logger.validate()) def test_unblinded_tokens_extracted( self, + logger, get_config, now, announcement, @@ -419,11 +439,12 @@ class ClientPluginTests(TestCase): ) # Give it enough for the allocate_buckets call below. - token_count = required_passes(BYTES_PER_PASS, [size] * len(sharenums)) + token_count = required_passes(store.pass_value, [size] * len(sharenums)) # And few enough redemption groups given the number of tokens. num_redemption_groups = token_count store = VoucherStore.from_node_config(node_config, lambda: now) + expected_pass_cost = controller = PaymentController( store, DummyRedeemer(), @@ -460,6 +481,23 @@ class ClientPluginTests(TestCase): raises(NotEnoughTokens), ) + messages = LoggedMessage.of_type(logger.messages, GET_PASSES) + self.assertThat( + messages, + MatchesAll( + HasLength(1), + AllMatch( + AfterPreprocessing( + lambda logged_message: logged_message.message, + ContainsDict({ + u"message": Equals(allocate_buckets_message(storage_index)), + u"count": Equals(expected_pass_cost), + }), + ), + ), + ), + ) + class ClientResourceTests(TestCase): """ diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py index 713a0c3e862dddfbf803e409898043f0cd562532..f2b9b6895246583018d498422e84ad2265e4eb4c 100644 --- a/src/_zkapauthorizer/tests/test_storage_protocol.py +++ b/src/_zkapauthorizer/tests/test_storage_protocol.py @@ -167,6 +167,8 @@ class ShareTests(TestCase): iteration of the test so far, probably; so make relative comparisons instead of absolute ones). """ + pass_value = 128 * 1024 + def setUp(self): super(ShareTests, self).setUp() self.canary = LocalReferenceable(None) @@ -187,10 +189,12 @@ class ShareTests(TestCase): ) self.server = ZKAPAuthorizerStorageServer( self.anonymous_storage_server, + self.pass_value, self.signing_key, ) self.local_remote_server = LocalRemote(self.server) self.client = ZKAPAuthorizerStorageClient( + self.pass_value, get_rref=lambda: self.local_remote_server, get_passes=get_passes, ) diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py index 55f4402da118c6eac3bf7717a6a690c742c3d835..88ae5a1f1294bc0679787942f7432aa7e08d2291 100644 --- a/src/_zkapauthorizer/tests/test_storage_server.py +++ b/src/_zkapauthorizer/tests/test_storage_server.py @@ -92,7 +92,6 @@ from ..api import ( MorePassesRequired, ) from ..storage_common import ( - BYTES_PER_PASS, required_passes, allocate_buckets_message, add_lease_message, @@ -107,6 +106,8 @@ class PassValidationTests(TestCase): """ Tests for pass validation performed by ``ZKAPAuthorizerStorageServer``. """ + pass_value = 128 * 1024 + @skipIf(platform.isWindows(), "Storage server is not supported on Windows") def setUp(self): super(PassValidationTests, self).setUp() @@ -119,6 +120,7 @@ class PassValidationTests(TestCase): self.signing_key = random_signing_key() self.storage_server = ZKAPAuthorizerStorageServer( self.anonymous_storage_server, + self.pass_value, self.signing_key, self.clock, ) @@ -162,7 +164,7 @@ class PassValidationTests(TestCase): required_passes = 2 share_nums = {3, 7} - allocated_size = int((required_passes * BYTES_PER_PASS) / len(share_nums)) + allocated_size = int((required_passes * self.pass_value) / len(share_nums)) storage_index = b"0123456789" renew_secret = b"x" * 32 cancel_secret = b"y" * 32 @@ -250,7 +252,7 @@ class PassValidationTests(TestCase): :param make_data_vector: A one-argument callable. It will be called with the current length of a slot share. It should return a write vector which will increase the storage requirements of that slot - share by at least BYTES_PER_PASS. + share by at least ``self.pass_value``. """ # hypothesis causes our storage server to be used many times. Clean # up between iterations. @@ -266,6 +268,7 @@ class PassValidationTests(TestCase): # print("test suite") required_pass_count = get_required_new_passes_for_mutable_write( + self.pass_value, dict.fromkeys(tw_vectors.keys(), 0), tw_vectors, ) @@ -355,7 +358,7 @@ class PassValidationTests(TestCase): test_and_write_vectors_for_shares, lambda current_length: ( [], - [(current_length, "x" * BYTES_PER_PASS)], + [(current_length, "x" * self.pass_value)], None, ), ) @@ -387,7 +390,7 @@ class PassValidationTests(TestCase): renew_secret, cancel_secret = secrets - required_count = required_passes(BYTES_PER_PASS, [allocated_size] * len(sharenums)) + required_count = required_passes(self.pass_value, [allocated_size] * len(sharenums)) # Create some shares at a slot which will require lease renewal. write_toy_shares( self.anonymous_storage_server, @@ -524,6 +527,7 @@ class PassValidationTests(TestCase): # Create an initial share to toy with. required_pass_count = get_required_new_passes_for_mutable_write( + self.pass_value, dict.fromkeys(tw_vectors.keys(), 0), tw_vectors, ) diff --git a/src/_zkapauthorizer/validators.py b/src/_zkapauthorizer/validators.py new file mode 100644 index 0000000000000000000000000000000000000000..bd1545144b3a9ed39d10c656ccd9ebbbde549804 --- /dev/null +++ b/src/_zkapauthorizer/validators.py @@ -0,0 +1,60 @@ +# Copyright 2019 PrivateStorage.io, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +This module implements validators for ``attrs``-defined attributes. +""" + +from base64 import ( + b64decode, +) + +def is_base64_encoded(b64decode=b64decode): + def validate_is_base64_encoded(inst, attr, value): + try: + b64decode(value.encode("ascii")) + except TypeError: + raise TypeError( + "{name!r} must be base64 encoded unicode, (got {value!r})".format( + name=attr.name, + value=value, + ), + ) + return validate_is_base64_encoded + +def has_length(expected): + def validate_has_length(inst, attr, value): + if len(value) != expected: + raise ValueError( + "{name!r} must have length {expected}, instead has length {actual}".format( + name=attr.name, + expected=expected, + actual=len(value), + ), + ) + return validate_has_length + +def greater_than(expected): + def validate_relation(inst, attr, value): + if value > expected: + return None + + raise ValueError( + "{name!r} must be greater than {expected}, instead it was {actual}".format( + name=attr.name, + expected=expected, + actual=value, + ), + ) + return validate_relation diff --git a/zkapauthorizer.nix b/zkapauthorizer.nix index 5b113d7357028fd3e38017eaf6cf46eb60cee3df..a5e611b4c8879f65aa12c63c968de225a96834ac 100644 --- a/zkapauthorizer.nix +++ b/zkapauthorizer.nix @@ -1,6 +1,6 @@ { lib , buildPythonPackage, sphinx, git -, attrs, zope_interface, aniso8601, twisted, tahoe-lafs, challenge-bypass-ristretto, treq +, attrs, zope_interface, eliot, aniso8601, twisted, tahoe-lafs, challenge-bypass-ristretto, treq , fixtures, testtools, hypothesis, pyflakes, coverage , hypothesisProfile ? null , collectCoverage ? false @@ -31,6 +31,7 @@ buildPythonPackage rec { attrs zope_interface aniso8601 + eliot twisted tahoe-lafs challenge-bypass-ristretto