diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index b403363d6d25b7e8c6d4c84c871c3c8b11865ecf..cf88d3c48de1e9306be711f9260465db41508e44 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -48,3 +48,13 @@ 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 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:: + + [storageserver.plugins.privatestorageio-zkapauthz-v1] + ristretto-signing-key-path = /path/to/signing.key + +The signing key is the keystone secret to the entire system and must be managed with extreme care to prevent unintended disclosure. +If things go well a future version of ZKAPAuthorizer will remove the requirement that the signing key be distributed to storage servers. diff --git a/setup.py b/setup.py index 606849326a4002007fd42060b51e69a19c18675c..f61c0015abc4af74c07c67a1f6127d032867aa66 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,7 @@ from setuptools import setup -setup() +setup( + package_data={ + "": ["testing-signing.key"], + }, +) diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index e5c59e23456605c95c36ed24839aa1a439957520..355642dbf1ae232099ae802fba3520b573b555a1 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -27,6 +27,9 @@ from zope.interface import ( implementer, ) +from twisted.python.filepath import ( + FilePath, +) from twisted.internet.defer import ( succeed, ) @@ -35,6 +38,9 @@ from allmydata.interfaces import ( IFoolscapStoragePlugin, IAnnounceableStorageServer, ) +from privacypass import ( + SigningKey, +) from .api import ( ZKAPAuthorizerStorageServer, @@ -104,11 +110,17 @@ 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") + signing_key = SigningKey.decode_base64( + FilePath( + kwargs.pop(u"ristretto-signing-key-path"), + ).getContent(), + ) announcement = { u"ristretto-issuer-root-url": root_url, } storage_server = ZKAPAuthorizerStorageServer( get_anonymous_storage_server(), + signing_key, **kwargs ) return succeed( diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py index 6b1c1acf3babf0d303802f75320dc0734e6cdde1..7eebdc56a6e567715b2909ed0e9c9a2c03660486 100644 --- a/src/_zkapauthorizer/_storage_client.py +++ b/src/_zkapauthorizer/_storage_client.py @@ -13,7 +13,7 @@ # limitations under the License. """ -A Tahoe-LAFS ``IStorageServer`` implementation which presents tokens +A Tahoe-LAFS ``IStorageServer`` implementation which presents passes per-call to prove authorization for writes and lease updates. This is the client part of a storage access protocol. The server part is @@ -30,19 +30,28 @@ from allmydata.interfaces import ( IStorageServer, ) +from .storage_common import ( + BYTES_PER_PASS, + required_passes, + allocate_buckets_message, + add_lease_message, + renew_lease_message, + slot_testv_and_readv_and_writev_message, +) + @implementer(IStorageServer) @attr.s class ZKAPAuthorizerStorageClient(object): """ - An implementation of the client portion of an access-token-based + An implementation of the client portion of an access-pass-based authorization scheme on top of the basic Tahoe-LAFS storage protocol. This ``IStorageServer`` implementation aims to offer the same storage functionality as Tahoe-LAFS' built-in storage server but with an added - layer of token-based authorization for some operations. The Python + layer of pass-based authorization for some operations. The Python interface exposed to application code is the same but the network protocol - is augmented with tokens which are automatically inserted by this class. - The tokens are interpreted by the corresponding server-side implementation + is augmented with passes which are automatically inserted by this class. + The passes are interpreted by the corresponding server-side implementation of this scheme. :ivar _get_rref: A no-argument callable which retrieves the most recently @@ -64,13 +73,16 @@ class ZKAPAuthorizerStorageClient(object): def _get_encoded_passes(self, message, count): """ + :param unicode message: The message to which to bind the passes. + :return: A list of passes from ``_get_passes`` encoded into their ``bytes`` representation. """ + assert isinstance(message, unicode) return list( t.text.encode("ascii") for t - in self._get_passes(message.encode("hex"), count) + in self._get_passes(message.encode("utf-8"), count) ) def get_version(self): @@ -89,7 +101,10 @@ class ZKAPAuthorizerStorageClient(object): ): return self._rref.callRemote( "allocate_buckets", - self._get_encoded_passes(storage_index, 1), + self._get_encoded_passes( + allocate_buckets_message(storage_index), + required_passes(BYTES_PER_PASS, sharenums, allocated_size), + ), storage_index, renew_secret, cancel_secret, @@ -115,7 +130,7 @@ class ZKAPAuthorizerStorageClient(object): ): return self._rref.callRemote( "add_lease", - self._get_encoded_passes(storage_index, 1), + self._get_encoded_passes(add_lease_message(storage_index), 1), storage_index, renew_secret, cancel_secret, @@ -128,7 +143,7 @@ class ZKAPAuthorizerStorageClient(object): ): return self._rref.callRemote( "renew_lease", - self._get_encoded_passes(storage_index, 1), + self._get_encoded_passes(renew_lease_message(storage_index), 1), storage_index, renew_secret, ) @@ -157,7 +172,7 @@ class ZKAPAuthorizerStorageClient(object): ): return self._rref.callRemote( "slot_testv_and_readv_and_writev", - self._get_encoded_passes(storage_index, 1), + self._get_encoded_passes(slot_testv_and_readv_and_writev_message(storage_index), 1), storage_index, secrets, tw_vectors, diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py index 3f9aec7c698a18913c45c86a1b8924111a3ab110..6d03290e7a3dc719a1c2c5fee00df03cdaae179d 100644 --- a/src/_zkapauthorizer/_storage_server.py +++ b/src/_zkapauthorizer/_storage_server.py @@ -14,7 +14,7 @@ """ A Tahoe-LAFS RIStorageServer-alike which authorizes writes and lease -updates using a per-call token. +updates using per-call passes. This is the server part of a storage access protocol. The client part is implemented in ``_storage_client.py``. @@ -27,12 +27,12 @@ from __future__ import ( import attr from attr.validators import ( provides, + instance_of, ) from zope.interface import ( implementer_only, ) - from foolscap.api import ( Referenceable, ) @@ -43,10 +43,37 @@ from foolscap.ipb import ( from allmydata.interfaces import ( RIStorageServer, ) - +from privacypass import ( + TokenPreimage, + VerificationSignature, + SigningKey, +) from .foolscap import ( RITokenAuthorizedStorageServer, ) +from .storage_common import ( + BYTES_PER_PASS, + required_passes, + allocate_buckets_message, + add_lease_message, + renew_lease_message, + slot_testv_and_readv_and_writev_message, +) + +class MorePassesRequired(Exception): + def __init__(self, valid_count, required_count): + self.valid_count = valid_count + self.required_count = required_count + + def __repr__(self): + return "MorePassedRequired(valid_count={}, required_count={})".format( + self.valid_count, + self.required_count, + ) + + def __str__(self): + return repr(self) + @implementer_only(RITokenAuthorizedStorageServer, IReferenceable, IRemotelyCallable) # It would be great to use `frozen=True` (value-based hashing) instead of @@ -55,85 +82,130 @@ from .foolscap import ( @attr.s(cmp=False) class ZKAPAuthorizerStorageServer(Referenceable): """ - A class which wraps an ``RIStorageServer`` to insert token validity checks + A class which wraps an ``RIStorageServer`` to insert pass validity checks before allowing certain functionality. """ _original = attr.ib(validator=provides(RIStorageServer)) + _signing_key = attr.ib(validator=instance_of(SigningKey)) - def _validate_tokens(self, tokens): + def _is_invalid_pass(self, message, pass_): """ - Check that all of the given tokens are valid. + Check the validity of a single pass. - :raise InvalidToken: If any token in ``tokens`` is not valid. + :param unicode message: The shared message for pass validation. + :param bytes pass_: The encoded pass to validate. - :return NoneType: If all of the tokens in ``tokens`` are valid. + :return bool: ``True`` if the pass is invalid, ``False`` otherwise. + """ + assert isinstance(message, unicode), "message %r not unicode" % (message,) + assert isinstance(pass_, bytes), "pass %r not bytes" % (pass_,) + try: + preimage_base64, signature_base64 = pass_.split(b" ") + preimage = TokenPreimage.decode_base64(preimage_base64) + proposed_signature = VerificationSignature.decode_base64(signature_base64) + unblinded_token = self._signing_key.rederive_unblinded_token(preimage) + verification_key = unblinded_token.derive_verification_key_sha512() + invalid_pass = verification_key.invalid_sha512(proposed_signature, message.encode("utf-8")) + return invalid_pass + except Exception: + # It would be pretty nice to log something here, sometimes, I guess? + return True + + def _validate_passes(self, message, passes): + """ + Check all of the given passes for validity. + + :param unicode message: The shared message for pass validation. + :param list[bytes] passes: The encoded passes to validate. - :note: This is yet to be implemented so it always returns ``None``. + :return list[bytes]: The passes which are found to be valid. """ - return None + return list( + pass_ + for pass_ + in passes + if not self._is_invalid_pass(message, pass_) + ) def remote_get_version(self): """ - Pass through without token check to allow clients to learn about our + Pass-through without pass check to allow clients to learn about our version and configuration in case it helps them decide how to behave. """ return self._original.remote_get_version() - def remote_allocate_buckets(self, tokens, *a, **kw): + def remote_allocate_buckets(self, passes, storage_index, renew_secret, cancel_secret, sharenums, allocated_size, canary): """ - Pass through after a token check to ensure that clients can only allocate - storage for immutable shares if they present valid tokens. + Pass-through after a pass check to ensure that clients can only allocate + storage for immutable shares if they present valid passes. """ - self._validate_tokens(tokens) - return self._original.remote_allocate_buckets(*a, **kw) + valid_passes = self._validate_passes( + allocate_buckets_message(storage_index), + passes, + ) + required_pass_count = required_passes(BYTES_PER_PASS, sharenums, allocated_size) + if len(valid_passes) < required_pass_count: + raise MorePassesRequired( + len(valid_passes), + required_pass_count, + ) + + return self._original.remote_allocate_buckets( + storage_index, + renew_secret, + cancel_secret, + sharenums, + allocated_size, + canary, + ) def remote_get_buckets(self, storage_index): """ - Pass through without token check to let clients read immutable shares as + Pass-through without pass check to let clients read immutable shares as long as those shares exist. """ return self._original.remote_get_buckets(storage_index) - def remote_add_lease(self, tokens, *a, **kw): + def remote_add_lease(self, passes, storage_index, *a, **kw): """ - Pass through after a token check to ensure clients can only extend the - duration of share storage if they present valid tokens. + Pass-through after a pass check to ensure clients can only extend the + duration of share storage if they present valid passes. """ - self._validate_tokens(tokens) - return self._original.remote_add_lease(*a, **kw) + self._validate_passes(add_lease_message(storage_index), passes) + return self._original.remote_add_lease(storage_index, *a, **kw) - def remote_renew_lease(self, tokens, *a, **kw): + def remote_renew_lease(self, passes, storage_index, *a, **kw): """ - Pass through after a token check to ensure clients can only extend the - duration of share storage if they present valid tokens. + Pass-through after a pass check to ensure clients can only extend the + duration of share storage if they present valid passes. """ - self._validate_tokens(tokens) - return self._original.remote_renew_lease(*a, **kw) + self._validate_passes(renew_lease_message(storage_index), passes) + return self._original.remote_renew_lease(storage_index, *a, **kw) def remote_advise_corrupt_share(self, *a, **kw): """ - Pass through without a token check to let clients inform us of possible + Pass-through without a pass check to let clients inform us of possible issues with the system without incurring any cost to themselves. """ return self._original.remote_advise_corrupt_share(*a, **kw) def remote_slot_testv_and_readv_and_writev( self, - tokens, + passes, storage_index, secrets, tw_vectors, r_vector, ): """ - Pass through after a token check to ensure clients can only allocate - storage for mutable shares if they present valid tokens. + Pass-through after a pass check to ensure clients can only allocate + storage for mutable shares if they present valid passes. :note: This method can be used both to allocate storage and to rewrite data in already-allocated storage. These cases may not be the - same from the perspective of token validation. + same from the perspective of pass validation. """ - self._validate_tokens(tokens) + self._validate_passes(slot_testv_and_readv_and_writev_message(storage_index), passes) # Skip over the remotely exposed method and jump to the underlying # implementation which accepts one additional parameter that we know # about (and don't expose over the network): renew_leases. We always @@ -149,7 +221,7 @@ class ZKAPAuthorizerStorageServer(Referenceable): def remote_slot_readv(self, *a, **kw): """ - Pass through without a token check to let clients read mutable shares as + Pass-through without a pass check to let clients read mutable shares as long as those shares exist. """ return self._original.remote_slot_readv(*a, **kw) diff --git a/src/_zkapauthorizer/api.py b/src/_zkapauthorizer/api.py index 81f47520ce66ffadb55a41fb3885d1cd50a7947c..8b89611ba10e3ee3833f2d5c7c45d1d8365ee320 100644 --- a/src/_zkapauthorizer/api.py +++ b/src/_zkapauthorizer/api.py @@ -13,12 +13,14 @@ # limitations under the License. __all__ = [ + "MorePassesRequired", "ZKAPAuthorizerStorageServer", "ZKAPAuthorizerStorageClient", "ZKAPAuthorizer", ] from ._storage_server import ( + MorePassesRequired, ZKAPAuthorizerStorageServer, ) from ._storage_client import ( diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py new file mode 100644 index 0000000000000000000000000000000000000000..955eb59928c337a4ada4881cf23f92977904dc57 --- /dev/null +++ b/src/_zkapauthorizer/storage_common.py @@ -0,0 +1,41 @@ + +from base64 import ( + b64encode, +) + +from math import ( + ceil, +) + +def _message_maker(label): + def make_message(storage_index): + return u"{label} {storage_index}".format( + label=label, + storage_index=b64encode(storage_index), + ) + return make_message + +allocate_buckets_message = _message_maker(u"allocate_buckets") +add_lease_message = _message_maker(u"add_lease") +renew_lease_message = _message_maker(u"renew_lease") +slot_testv_and_readv_and_writev_message = _message_maker(u"slot_testv_and_readv_and_writev") + +# The number of bytes we're willing to store for a lease period for each pass +# submitted. +BYTES_PER_PASS = 128 * 1024 + +def required_passes(bytes_per_pass, share_nums, share_size): + """ + Calculate the number of passes that are required to store ``stored_bytes`` + for one lease period. + + :param int stored_bytes: A number of bytes of storage for which to + calculate a price in passes. + + :return int: The number of passes. + """ + return int( + ceil( + (len(share_nums) * share_size) / bytes_per_pass, + ), + ) diff --git a/src/_zkapauthorizer/tests/fixtures.py b/src/_zkapauthorizer/tests/fixtures.py new file mode 100644 index 0000000000000000000000000000000000000000..20b05922495aefce85e12a69941276836840950a --- /dev/null +++ b/src/_zkapauthorizer/tests/fixtures.py @@ -0,0 +1,34 @@ +from __future__ import ( + absolute_import, +) + +from fixtures import ( + Fixture, + TempDir, +) + +from twisted.python.filepath import ( + FilePath, +) + +from allmydata.storage.server import ( + StorageServer, +) + +class AnonymousStorageServer(Fixture): + """ + Supply an instance of allmydata.storage.server.StorageServer which + implements anonymous access to Tahoe-LAFS storage server functionality. + + :ivar FilePath tempdir: The path to the server's storage on the + filesystem. + + :ivar allmydata.storage.server.StorageServer storage_server: The storage + server. + """ + def _setUp(self): + self.tempdir = FilePath(self.useFixture(TempDir()).join(b"storage")) + self.storage_server = StorageServer( + self.tempdir.asBytesMode().path, + b"x" * 20, + ) diff --git a/src/_zkapauthorizer/tests/privacypass.py b/src/_zkapauthorizer/tests/privacypass.py new file mode 100644 index 0000000000000000000000000000000000000000..9b46fe3832bf1ce57a6852037e9bca5c3fb1fb37 --- /dev/null +++ b/src/_zkapauthorizer/tests/privacypass.py @@ -0,0 +1,55 @@ +from __future__ import ( + absolute_import, +) + +from privacypass import ( + BatchDLEQProof, + PublicKey, +) + +def make_passes(signing_key, for_message, random_tokens): + blinded_tokens = list( + token.blind() + for token + in random_tokens + ) + signatures = list( + signing_key.sign(blinded_token) + for blinded_token + in blinded_tokens + ) + proof = BatchDLEQProof.create( + signing_key, + blinded_tokens, + signatures, + ) + unblinded_signatures = proof.invalid_or_unblind( + random_tokens, + blinded_tokens, + signatures, + PublicKey.from_signing_key(signing_key), + ) + preimages = list( + unblinded_signature.preimage() + for unblinded_signature + in unblinded_signatures + ) + verification_keys = list( + unblinded_signature.derive_verification_key_sha512() + for unblinded_signature + in unblinded_signatures + ) + message_signatures = list( + verification_key.sign_sha512(for_message.encode("utf-8")) + for verification_key + in verification_keys + ) + passes = list( + u"{} {}".format( + preimage.encode_base64().decode("ascii"), + signature.encode_base64().decode("ascii"), + ).encode("ascii") + for (preimage, signature) + in zip(preimages, message_signatures) + ) + return passes diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index 0af3d1f86dc76db4b2e2627eedde8972e85a2338..5a0c531a6448bda2c59d40deeb9aa9e3523be77e 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -164,12 +164,16 @@ def node_nicknames(): ) -def server_configurations(): +def server_configurations(signing_key_path): """ Build configuration values for the server-side plugin. + + :param unicode signing_key_path: A value to insert for the + **ristretto-signing-key-path** item. """ return just({ u"ristretto-issuer-root-url": u"https://issuer.example.invalid/", + u"ristretto-signing-key-path": signing_key_path.path, }) @@ -214,13 +218,10 @@ def zkaps(): """ Build random ZKAPs as ``Pass` instances. """ - return binary( - min_size=32, - max_size=32, - ).map( - urlsafe_b64encode, - ).map( - lambda zkap: Pass(zkap.decode("ascii")), + return builds( + lambda preimage, signature: Pass(u"{} {}".format(preimage, signature)), + preimage=binary(min_size=66, max_size=66).map(urlsafe_b64encode), + signature=binary(min_size=66, max_size=66).map(urlsafe_b64encode), ) diff --git a/src/_zkapauthorizer/tests/test_base64.py b/src/_zkapauthorizer/tests/test_base64.py index 05501c6caae6d185a413a9d5876d78c0c5df9322..4a988b6beae887138a8379210f2e76cae0aaab53 100644 --- a/src/_zkapauthorizer/tests/test_base64.py +++ b/src/_zkapauthorizer/tests/test_base64.py @@ -16,6 +16,10 @@ Tests for ``_zkapauthorizer._base64``. """ +from __future__ import ( + absolute_import, +) + from base64 import ( urlsafe_b64encode, ) diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 77b5bb38228a73044f7c08f98d460a024138c8b2..20047d3cf2b986112c386dd99e1cf782d87cb573 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -17,6 +17,10 @@ Tests for the web resource provided by the client part of the Tahoe-LAFS plugin. """ +from __future__ import ( + absolute_import, +) + import attr from .._base64 import ( diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index 4e3f264ce22a05d32535c87888d19d2af93d0147..6001b29a5d0f824d655c6f24d9fa039e70645577 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -16,6 +16,10 @@ Tests for ``_zkapauthorizer.controller``. """ +from __future__ import ( + absolute_import, +) + from json import ( loads, dumps, diff --git a/src/_zkapauthorizer/tests/test_matchers.py b/src/_zkapauthorizer/tests/test_matchers.py index 7ff71c0fd360daefab6a0485dd62abbf4bc4d187..868a1e998bec4a54c093f24637a5a3f8ecee6261 100644 --- a/src/_zkapauthorizer/tests/test_matchers.py +++ b/src/_zkapauthorizer/tests/test_matchers.py @@ -16,6 +16,10 @@ Tests for ``_zkapauthorizer.tests.matchers``. """ +from __future__ import ( + absolute_import, +) + from zope.interface import ( Interface, implementer, diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index f614c1c912823cec16cf9db27a546c46d27a7b9b..14fd2f0a7339fd264ff6ba5e492f55adad6a8b20 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -17,6 +17,10 @@ Tests for ``_zkapauthorizer.model``. """ +from __future__ import ( + absolute_import, +) + from os import ( mkdir, ) diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index d497c724877505caf6ae95cbc451e429fbb2d384..0257683da1813e697407d9336ffdd4880ad62147 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -16,6 +16,10 @@ Tests for the Tahoe-LAFS plugin. """ +from __future__ import ( + absolute_import, +) + from io import ( BytesIO, ) @@ -67,6 +71,9 @@ from allmydata.interfaces import ( RIStorageServer, ) +from twisted.python.filepath import ( + FilePath, +) from twisted.plugin import ( getPlugins, ) @@ -103,6 +110,9 @@ from .matchers import ( ) +SIGNING_KEY_PATH = FilePath(__file__).sibling(u"testing-signing.key") + + @implementer(RIStorageServer) class StubStorageServer(object): pass @@ -146,7 +156,7 @@ class ServerPluginTests(TestCase): Tests for the plugin's implementation of ``IFoolscapStoragePlugin.get_storage_server``. """ - @given(server_configurations()) + @given(server_configurations(SIGNING_KEY_PATH)) def test_returns_announceable(self, configuration): """ ``storage_server.get_storage_server`` returns an instance which provides @@ -162,7 +172,7 @@ class ServerPluginTests(TestCase): ) - @given(server_configurations()) + @given(server_configurations(SIGNING_KEY_PATH)) def test_returns_referenceable(self, configuration): """ The storage server attached to the result of @@ -183,7 +193,7 @@ class ServerPluginTests(TestCase): ), ) - @given(server_configurations()) + @given(server_configurations(SIGNING_KEY_PATH)) def test_returns_serializable(self, configuration): """ The storage server attached to the result of @@ -207,7 +217,7 @@ class ServerPluginTests(TestCase): ) - @given(server_configurations()) + @given(server_configurations(SIGNING_KEY_PATH)) def test_returns_hashable(self, configuration): """ The storage server attached to the result of diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py index 0bd6cb80835986f16bee3f4eacfa72f635608bec..98320fdd1581c2a537bc505492c597ddceb0c3e8 100644 --- a/src/_zkapauthorizer/tests/test_storage_protocol.py +++ b/src/_zkapauthorizer/tests/test_storage_protocol.py @@ -16,11 +16,13 @@ Tests for communication between the client and server components. """ +from __future__ import ( + absolute_import, +) + import attr from fixtures import ( - Fixture, - TempDir, MonkeyPatch, ) from testtools import ( @@ -62,10 +64,14 @@ from foolscap.referenceable import ( LocalReferenceable, ) -from allmydata.storage.server import ( - StorageServer, +from privacypass import ( + RandomToken, + random_signing_key, ) +from .privacypass import ( + make_passes, +) from .strategies import ( storage_indexes, lease_renew_secrets, @@ -81,36 +87,20 @@ from .strategies import ( from .matchers import ( matches_version_dictionary, ) +from .fixtures import ( + AnonymousStorageServer, +) from ..api import ( ZKAPAuthorizerStorageServer, ZKAPAuthorizerStorageClient, ) -from ..foolscap import ( - TOKEN_LENGTH, +from ..storage_common import ( + slot_testv_and_readv_and_writev_message, ) from ..model import ( Pass, ) -class AnonymousStorageServer(Fixture): - """ - Supply an instance of allmydata.storage.server.StorageServer which - implements anonymous access to Tahoe-LAFS storage server functionality. - - :ivar FilePath tempdir: The path to the server's storage on the - filesystem. - - :ivar allmydata.storage.server.StorageServer storage_server: The storage - server. - """ - def _setUp(self): - self.tempdir = FilePath(self.useFixture(TempDir()).join(b"storage")) - self.storage_server = StorageServer( - self.tempdir.asBytesMode().path, - b"x" * 20, - ) - - @attr.s class LocalRemote(object): """ @@ -159,19 +149,21 @@ class ShareTests(TestCase): super(ShareTests, self).setUp() self.canary = LocalReferenceable(None) self.anonymous_storage_server = self.useFixture(AnonymousStorageServer()).storage_server + self.signing_key = random_signing_key() def get_passes(message, count): - if not isinstance(message, bytes): - raise TypeError("message must be bytes") - try: - message.decode("utf-8") - except UnicodeDecodeError: - raise TypeError("message must be valid utf-8") - - return [Pass(u"x" * TOKEN_LENGTH)] * count - + return list( + Pass(pass_.decode("ascii")) + for pass_ + in make_passes( + self.signing_key, + message, + list(RandomToken.create() for n in range(count)), + ) + ) self.server = ZKAPAuthorizerStorageServer( self.anonymous_storage_server, + self.signing_key, ) self.local_remote_server = LocalRemote(self.server) self.client = ZKAPAuthorizerStorageClient( @@ -520,7 +512,10 @@ class ShareTests(TestCase): d = self.client._rref.callRemote( "slot_testv_and_readv_and_writev", # passes - self.client._get_encoded_passes(storage_index, 1), + self.client._get_encoded_passes( + slot_testv_and_readv_and_writev_message(storage_index), + 1, + ), # storage_index storage_index, # secrets diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py new file mode 100644 index 0000000000000000000000000000000000000000..32ebcc42e78056a39926ee5650b99d74503eeb79 --- /dev/null +++ b/src/_zkapauthorizer/tests/test_storage_server.py @@ -0,0 +1,121 @@ +from __future__ import ( + absolute_import, + division, +) + +from random import ( + shuffle, +) +from testtools import ( + TestCase, +) +from testtools.matchers import ( + Equals, + AfterPreprocessing, + raises, +) +from hypothesis import ( + given, +) +from hypothesis.strategies import ( + integers, + lists, +) +from privacypass import ( + RandomToken, + random_signing_key, +) +from foolscap.referenceable import ( + LocalReferenceable, +) + +from .privacypass import ( + make_passes, +) +from .strategies import ( + zkaps, +) +from .fixtures import ( + AnonymousStorageServer, +) +from ..api import ( + ZKAPAuthorizerStorageServer, + MorePassesRequired, +) +from ..storage_common import ( + BYTES_PER_PASS, + allocate_buckets_message, +) + + +class PassValidationTests(TestCase): + """ + Tests for pass validation performed by ``ZKAPAuthorizerStorageServer``. + """ + def setUp(self): + super(PassValidationTests, self).setUp() + self.anonymous_storage_server = self.useFixture(AnonymousStorageServer()).storage_server + self.signing_key = random_signing_key() + self.storage_server = ZKAPAuthorizerStorageServer( + self.anonymous_storage_server, + self.signing_key, + ) + + @given(integers(min_value=0, max_value=64), lists(zkaps(), max_size=64)) + def test_validation_result(self, valid_count, invalid_passes): + """ + ``_get_valid_passes`` returns the number of cryptographically valid passes + in the list passed to it. + """ + message = u"hello world" + valid_passes = make_passes( + self.signing_key, + message, + list(RandomToken.create() for i in range(valid_count)), + ) + all_passes = valid_passes + list(pass_.text.encode("ascii") for pass_ in invalid_passes) + shuffle(all_passes) + + self.assertThat( + self.storage_server._validate_passes(message, all_passes), + AfterPreprocessing( + set, + Equals(set(valid_passes)), + ), + ) + + + def test_allocate_buckets_fails_without_enough_passes(self): + """ + ``remote_allocate_buckets`` fails with ``MorePassesRequired`` if it is + passed fewer passes than it requires for the amount of data to be + stored. + """ + required_passes = 2 + share_nums = {3, 7} + allocated_size = int((required_passes * BYTES_PER_PASS) / len(share_nums)) + storage_index = b"0123456789" + renew_secret = b"x" * 32 + cancel_secret = b"y" * 32 + valid_passes = make_passes( + self.signing_key, + allocate_buckets_message(storage_index), + list(RandomToken.create() for i in range(required_passes - 1)), + ) + + allocate_buckets = lambda: self.storage_server.doRemoteCall( + "allocate_buckets", + (valid_passes, + storage_index, + renew_secret, + cancel_secret, + share_nums, + allocated_size, + LocalReferenceable(None), + ), + {}, + ) + self.assertThat( + allocate_buckets, + raises(MorePassesRequired), + ) diff --git a/src/_zkapauthorizer/tests/test_strategies.py b/src/_zkapauthorizer/tests/test_strategies.py index 22f5aacfa67f0a06f886122f4d30856b718b8eac..6a0307ea4b799204dba2c4f297473a0d52619215 100644 --- a/src/_zkapauthorizer/tests/test_strategies.py +++ b/src/_zkapauthorizer/tests/test_strategies.py @@ -16,6 +16,10 @@ Tests for our custom Hypothesis strategies. """ +from __future__ import ( + absolute_import, +) + from testtools import ( TestCase, ) diff --git a/src/_zkapauthorizer/tests/testing-signing.key b/src/_zkapauthorizer/tests/testing-signing.key new file mode 100644 index 0000000000000000000000000000000000000000..078e2134bd719832938b965b0288e4246c364d2d --- /dev/null +++ b/src/_zkapauthorizer/tests/testing-signing.key @@ -0,0 +1 @@ +mkQf85V2vyLQRUYuqRb+Ke6K+M9pOtXm4MslsuCdBgg= \ No newline at end of file