diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst new file mode 100644 index 0000000000000000000000000000000000000000..75bf5efc10320c156f5fa65de065de14e6d050f2 --- /dev/null +++ b/docs/source/configuration.rst @@ -0,0 +1,6 @@ + +[storageclient.plugins.privatestorageio-zkapauthz-v1] +redeemer = < dummy | ristretto > + +[storageserver.plugins.privatestorageio-zkapauthz-v1] +ristretto-issuer-root-url = https://issuer.privatestorage.io/ diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index 1584a19dc169c70a2ed4a53f8b2d09aafa58ad53..b6e230c5d211b08c4882f9b01d3d4e15c11b4426 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -21,10 +21,6 @@ from weakref import ( WeakValueDictionary, ) -from functools import ( - partial, -) - import attr from zope.interface import ( @@ -54,7 +50,7 @@ from .resource import ( ) from .controller import ( - DummyRedeemer, + get_redeemer, ) @implementer(IAnnounceableStorageServer) @@ -96,11 +92,24 @@ class ZKAPAuthorizer(object): return s + def _get_redeemer(self, node_config, announcement, reactor): + """ + :return IRedeemer: The voucher redeemer indicated by the given + configuration. A new instance is returned on every call because + the redeemer interface is stateless. + """ + return get_redeemer(self.name, node_config, announcement, reactor) + + def get_storage_server(self, configuration, get_anonymous_storage_server): - announcement = {} + kwargs = configuration.copy() + root_url = kwargs.pop(u"ristretto-issuer-root-url") + announcement = { + u"ristretto-issuer-root-url": root_url, + } storage_server = ZKAPAuthorizerStorageServer( get_anonymous_storage_server(), - **configuration + **kwargs ) return succeed( AnnounceableStorageServer( @@ -117,16 +126,26 @@ class ZKAPAuthorizer(object): managed by this plugin in the node directory that goes along with ``node_config``. """ + from twisted.internet import reactor + redeemer = self._get_redeemer(node_config, announcement, reactor) + extract_unblinded_tokens = self._get_store(node_config).extract_unblinded_tokens + # TODO: Make the caller figure out the correct number to extract. + def get_passes(message, count=1): + unblinded_tokens = extract_unblinded_tokens(count) + return redeemer.tokens_to_passes(message, unblinded_tokens) + return ZKAPAuthorizerStorageClient( get_rref, - # TODO: Make the caller figure out the correct number to extract. - partial(self._get_store(node_config).extract_unblinded_tokens, 1), + get_passes, ) def get_client_resource(self, node_config): + # XXXXXXXXXXXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx........................ :( + announcement = {} + from twisted.internet import reactor return resource_from_configuration( node_config, store=self._get_store(node_config), - redeemer=DummyRedeemer(), + redeemer=self._get_redeemer(node_config, announcement, reactor), ) diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py index e0dc41ddca2693016c17785a9dc4b655b7f2b585..55ccdb0c5db4d30e6977a90fad7f1f4de08d7a22 100644 --- a/src/_zkapauthorizer/_storage_client.py +++ b/src/_zkapauthorizer/_storage_client.py @@ -49,8 +49,11 @@ class ZKAPAuthorizerStorageClient(object): valid ``RemoteReference`` corresponding to the server-side object for this scheme. - :ivar _get_passes: A no-argument callable which retrieves some passes - which can be used to authorize an operation. + :ivar _get_passes: A two-argument callable which retrieves some passes + which can be used to authorize an operation. The first argument is a + bytes message binding the passes to the request for which they will be + used. The second is an integer giving the number of passes to + request. """ _get_rref = attr.ib() _get_passes = attr.ib() @@ -59,7 +62,7 @@ class ZKAPAuthorizerStorageClient(object): def _rref(self): return self._get_rref() - def _get_encoded_passes(self): + def _get_encoded_passes(self, message, count): """ :return: A list of passes from ``_get_passes`` encoded into their ``bytes`` representation. @@ -67,7 +70,7 @@ class ZKAPAuthorizerStorageClient(object): return list( t.text.encode("ascii") for t - in self._get_passes() + in self._get_passes(message, count) ) def get_version(self): @@ -86,7 +89,7 @@ class ZKAPAuthorizerStorageClient(object): ): return self._rref.callRemote( "allocate_buckets", - self._get_encoded_passes(), + self._get_encoded_passes(storage_index, 1), storage_index, renew_secret, cancel_secret, @@ -112,7 +115,7 @@ class ZKAPAuthorizerStorageClient(object): ): return self._rref.callRemote( "add_lease", - self._get_encoded_passes(), + self._get_encoded_passes(storage_index, 1), storage_index, renew_secret, cancel_secret, @@ -125,7 +128,7 @@ class ZKAPAuthorizerStorageClient(object): ): return self._rref.callRemote( "renew_lease", - self._get_encoded_passes(), + self._get_encoded_passes(storage_index, 1), storage_index, renew_secret, ) @@ -154,7 +157,7 @@ class ZKAPAuthorizerStorageClient(object): ): return self._rref.callRemote( "slot_testv_and_readv_and_writev", - self._get_encoded_passes(), + self._get_encoded_passes(storage_index, 1), storage_index, secrets, tw_vectors, diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index f415824d732f77c09b4afd7fc26d2b348053adb4..c2a890ca8c68a630d267cec01113423a1c448c80 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -39,9 +39,15 @@ from twisted.internet.defer import ( inlineCallbacks, returnValue, ) +from twisted.web.client import ( + Agent, +) from treq import ( json_content, ) +from treq.client import ( + HTTPClient, +) import privacypass @@ -49,6 +55,7 @@ from .model import ( RandomToken, UnblindedToken, Voucher, + Pass, ) @@ -131,6 +138,10 @@ class DummyRedeemer(object): really redeeming them, it makes up some fake ZKAPs and pretends those are the result. """ + @classmethod + def make(cls, section_name, node_config, announcement, reactor): + return cls() + def random_tokens_for_voucher(self, voucher, count): """ Generate some number of random tokens to submit along with a voucher for @@ -156,6 +167,13 @@ class DummyRedeemer(object): ), ) + def tokens_to_passes(self, message, unblinded_tokens): + return list( + Pass(token.text) + for token + in unblinded_tokens + ) + @implementer(IRedeemer) @attr.s @@ -163,6 +181,13 @@ class RistrettoRedeemer(object): _treq = attr.ib() _api_root = attr.ib(validator=attr.validators.instance_of(URL)) + @classmethod + def make(cls, section_name, node_config, announcement, reactor): + return cls( + HTTPClient(Agent(reactor)), + URL.from_text(announcement[u"ristretto-issuer-root-url"]), + ) + def random_tokens_for_voucher(self, voucher, count): return list( RandomToken(privacypass.RandomToken.create().encode_base64().decode("ascii")) @@ -220,8 +245,9 @@ class RistrettoRedeemer(object): )) def tokens_to_passes(self, message, unblinded_tokens): - # XXX Here's some more of the privacypass dance. Something needs to - # know to call this, I guess? Also it's untested as heck. + assert isinstance(message, bytes) + assert isinstance(unblinded_tokens, list) + assert all(isinstance(element, UnblindedToken) for element in unblinded_tokens) unblinded_tokens = list( privacypass.UnblindedToken.decode_base64(token.text.encode("ascii")) for token @@ -252,7 +278,11 @@ class RistrettoRedeemer(object): for (token_preimage, sig) in clients_passes ) - return marshaled_passes + return list( + Pass(p) + for p + in marshaled_passes + ) @attr.s @@ -309,3 +339,19 @@ class PaymentController(object): passes later). """ self.store.insert_unblinded_tokens_for_voucher(voucher, unblinded_tokens) + + +def get_redeemer(plugin_name, node_config, announcement, reactor): + section_name = u"storageclient.plugins.{}".format(plugin_name) + redeemer_kind = node_config.get_config( + section=section_name, + option=u"redeemer", + default=u"ristretto", + ) + return _REDEEMERS[redeemer_kind](section_name, node_config, announcement, reactor) + + +_REDEEMERS = { + u"dummy": DummyRedeemer.make, + u"ristretto": RistrettoRedeemer.make, +} diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index f35371cc1a4f904ddaeed4bfc71293a3930e8688..a57170df008a055d7f71256e80343f707372ebca 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -119,7 +119,7 @@ def tahoe_config_texts(storage_client_plugins): ) -def tahoe_configs(storage_client_plugins=None): +def minimal_tahoe_configs(storage_client_plugins=None): """ Build complete Tahoe-LAFS configurations for a node. """ @@ -135,6 +135,17 @@ def tahoe_configs(storage_client_plugins=None): ), ) + +def tahoe_configs(): + """ + Build complete Tahoe-LAFS configurations including the zkapauthorizer + client plugin section. + """ + return minimal_tahoe_configs({ + u"privatestorageio-zkapauthz-v1": client_configurations(), + }) + + def node_nicknames(): """ Builds Tahoe-LAFS node nicknames. @@ -153,11 +164,13 @@ def node_nicknames(): ) -def configurations(): +def server_configurations(): """ Build configuration values for the server-side plugin. """ - return just({}) + return just({ + u"ristretto-issuer-root-url": u"https://issuer.example.invalid/", + }) def client_configurations(): @@ -435,4 +448,6 @@ def announcements(): """ Build announcements for the ZKAPAuthorizer plugin. """ - return just({}) + return just({ + u"ristretto-issuer-root-url": u"https://issuer.example.invalid/", + }) diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py index 203e44ca3ca557788daefd667fbbea3dc73de4e5..77b5bb38228a73044f7c08f98d460a024138c8b2 100644 --- a/src/_zkapauthorizer/tests/test_client_resource.py +++ b/src/_zkapauthorizer/tests/test_client_resource.py @@ -102,7 +102,6 @@ from ..resource import ( from .strategies import ( tahoe_configs, - client_configurations, vouchers, requests, ) @@ -121,10 +120,6 @@ def uncooperator(started=True): -tahoe_configs_with_client_config = tahoe_configs(storage_client_plugins={ - u"privatestorageio-zkapauthz-v1": client_configurations(), -}) - def is_not_json(bytestring): """ :param bytes bytestring: A candidate byte string to inspect. @@ -217,7 +212,7 @@ class VoucherTests(TestCase): self.useFixture(CaptureTwistedLogs()) - @given(tahoe_configs_with_client_config, requests(just([u"voucher"]))) + @given(tahoe_configs(), requests(just([u"voucher"]))) def test_reachable(self, get_config, request): """ A resource is reachable at the ``voucher`` child of a the resource @@ -232,7 +227,7 @@ class VoucherTests(TestCase): ) - @given(tahoe_configs_with_client_config, vouchers()) + @given(tahoe_configs(), vouchers()) def test_put_voucher(self, get_config, voucher): """ When a voucher is ``PUT`` to ``VoucherCollection`` it is passed in to the @@ -263,7 +258,7 @@ class VoucherTests(TestCase): ), ) - @given(tahoe_configs_with_client_config, invalid_bodies()) + @given(tahoe_configs(), invalid_bodies()) def test_put_invalid_body(self, get_config, body): """ If the body of a ``PUT`` to ``VoucherCollection`` does not consist of an @@ -294,7 +289,7 @@ class VoucherTests(TestCase): ), ) - @given(tahoe_configs_with_client_config, not_vouchers()) + @given(tahoe_configs(), not_vouchers()) def test_get_invalid_voucher(self, get_config, not_voucher): """ When a syntactically invalid voucher is requested with a ``GET`` to a @@ -322,7 +317,7 @@ class VoucherTests(TestCase): ) - @given(tahoe_configs_with_client_config, vouchers()) + @given(tahoe_configs(), vouchers()) def test_get_unknown_voucher(self, get_config, voucher): """ When a voucher is requested with a ``GET`` to a child of @@ -345,7 +340,7 @@ class VoucherTests(TestCase): ) - @given(tahoe_configs_with_client_config, vouchers()) + @given(tahoe_configs(), vouchers()) def test_get_known_voucher(self, get_config, voucher): """ When a voucher is first ``PUT`` and then later a ``GET`` is issued for the @@ -398,7 +393,7 @@ class VoucherTests(TestCase): ), ) - @given(tahoe_configs_with_client_config, lists(vouchers(), unique=True)) + @given(tahoe_configs(), lists(vouchers(), unique=True)) def test_list_vouchers(self, get_config, vouchers): """ A ``GET`` to the ``VoucherCollection`` itself returns a list of existing diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index 8c0f648d77f7bf80c5fab8e5fe72d498faf9a1dd..b37cdbf6692dadb2657db85a4469703c4642cce5 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -36,11 +36,12 @@ from testtools.matchers import ( from testtools.twistedsupport import ( succeeded, ) - from hypothesis import ( given, ) - +from hypothesis.strategies import ( + just, +) from foolscap.broker import ( Broker, ) @@ -77,8 +78,9 @@ from ..model import ( ) from .strategies import ( + minimal_tahoe_configs, tahoe_configs, - configurations, + server_configurations, announcements, vouchers, random_tokens, @@ -134,7 +136,7 @@ class ServerPluginTests(TestCase): Tests for the plugin's implementation of ``IFoolscapStoragePlugin.get_storage_server``. """ - @given(configurations()) + @given(server_configurations()) def test_returns_announceable(self, configuration): """ ``storage_server.get_storage_server`` returns an instance which provides @@ -150,7 +152,7 @@ class ServerPluginTests(TestCase): ) - @given(configurations()) + @given(server_configurations()) def test_returns_referenceable(self, configuration): """ The storage server attached to the result of @@ -171,7 +173,7 @@ class ServerPluginTests(TestCase): ), ) - @given(configurations()) + @given(server_configurations()) def test_returns_serializable(self, configuration): """ The storage server attached to the result of @@ -195,7 +197,7 @@ class ServerPluginTests(TestCase): ) - @given(configurations()) + @given(server_configurations()) def test_returns_hashable(self, configuration): """ The storage server attached to the result of @@ -221,6 +223,10 @@ class ServerPluginTests(TestCase): ) +tahoe_configs_with_dummy_redeemer = minimal_tahoe_configs({ + u"privatestorageio-zkapauthz-v1": just({u"redeemer": u"dummy"}), +}) + class ClientPluginTests(TestCase): """ @@ -252,7 +258,7 @@ class ClientPluginTests(TestCase): @given( - tahoe_configs(), + tahoe_configs_with_dummy_redeemer, announcements(), vouchers(), random_tokens(), diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py index 51cac393c395cdd3dc520aacc864dc81028e6d3b..73f9017efa02cc256b05dc224ee6ef461b5488b7 100644 --- a/src/_zkapauthorizer/tests/test_storage_protocol.py +++ b/src/_zkapauthorizer/tests/test_storage_protocol.py @@ -160,8 +160,8 @@ class ShareTests(TestCase): self.canary = LocalReferenceable(None) self.anonymous_storage_server = self.useFixture(AnonymousStorageServer()).storage_server - def get_passes(): - return [Pass(u"x" * TOKEN_LENGTH)] + def get_passes(message, count): + return [Pass(u"x" * TOKEN_LENGTH)] * count self.server = ZKAPAuthorizerStorageServer( self.anonymous_storage_server, @@ -512,8 +512,8 @@ class ShareTests(TestCase): # transit the network differently from keyword arguments. Yay. d = self.client._rref.callRemote( "slot_testv_and_readv_and_writev", - # tokens - self.client._get_encoded_passes(), + # passes + self.client._get_encoded_passes(storage_index, 1), # storage_index storage_index, # secrets