diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 75bf5efc10320c156f5fa65de065de14e6d050f2..c20c656b21772acdb8f6e8a550e739c423fe16ba 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -1,6 +1,7 @@ [storageclient.plugins.privatestorageio-zkapauthz-v1] redeemer = < dummy | ristretto > +ristretto-issuer-root-url = https://issuer.privatestorage.io/ [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 ac0b87e491d2193b1ecec144db22199b23d6c7b3..e5c59e23456605c95c36ed24839aa1a439957520 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -77,7 +77,6 @@ class ZKAPAuthorizer(object): """ name = attr.ib(default=u"privatestorageio-zkapauthz-v1") _stores = attr.ib(default=attr.Factory(WeakValueDictionary)) - _announcement = attr.ib(default=None) def _get_store(self, node_config): """ @@ -127,9 +126,6 @@ class ZKAPAuthorizer(object): managed by this plugin in the node directory that goes along with ``node_config``. """ - # XXXXXXXXXXXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx........................ :( - self._announcement = announcement - from twisted.internet import reactor redeemer = self._get_redeemer(node_config, announcement, reactor) extract_unblinded_tokens = self._get_store(node_config).extract_unblinded_tokens @@ -144,11 +140,9 @@ class ZKAPAuthorizer(object): def get_client_resource(self, node_config): - # XXXXXXXXXXXXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx........................ :( - announcement = self._announcement from twisted.internet import reactor return resource_from_configuration( node_config, store=self._get_store(node_config), - redeemer=self._get_redeemer(node_config, announcement, reactor), + redeemer=self._get_redeemer(node_config, None, reactor), ) diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index 457ed378065e347057126f54bfc34808151c2c4d..f6c38f23004131ab91eb827d3ef1db5fec04021e 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -178,6 +178,32 @@ class DummyRedeemer(object): ) +class IssuerConfigurationMismatch(Exception): + """ + The Ristretto issuer address in the local client configuration does not + match the Ristretto issuer address received in a storage server + announcement. + + If these values do not match then there is no reason to expect that ZKAPs + will be accepted by the storage server because ZKAPs are bound to the + issuer's signing key. + + This mismatch must be corrected before the storage server can be used. + Either the storage server needs to be reconfigured to respect the + authority of a different issuer (the same one the client is configured to + use), the client needs to select a different storage server to talk to, or + the client needs to be reconfigured to respect the authority of a + different issuer (the same one the storage server is announcing). + + Note that issued ZKAPs cannot be exchanged between issues except through + some ad hoc, out-of-band means. That is, if the client already has some + ZKAPs and chooses to change its configured issuer address, those existing + ZKAPs will not be usable and new ones must be obtained. + """ + def __str__(self): + return "Announced issuer ({}) disagrees with configured issuer ({}).".format(self.args) + + @implementer(IRedeemer) @attr.s class RistrettoRedeemer(object): @@ -188,9 +214,27 @@ class RistrettoRedeemer(object): @classmethod def make(cls, section_name, node_config, announcement, reactor): + configured_issuer = node_config.get_config( + section=section_name, + option=u"ristretto-issuer-root-url", + ).decode("ascii") + if announcement is not None: + # Don't let us talk to a storage server that has a different idea + # about who issues ZKAPs. We should lift this limitation (that is, we + # should support as many different issuers as the user likes) in the + # future but doing so requires changing how the web interface works + # and possibly also the interface for voucher submission. + # + # If we aren't given an announcement then we're not being used in + # the context of a specific storage server so the check is + # unnecessary and impossible. + announced_issuer = announcement[u"ristretto-issuer-root-url"] + if announced_issuer != configured_issuer: + raise IssuerConfigurationMismatch(announced_issuer, configured_issuer) + return cls( HTTPClient(Agent(reactor)), - URL.from_text(announcement[u"ristretto-issuer-root-url"]), + URL.from_text(configured_issuer), ) def random_tokens_for_voucher(self, voucher, count): diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index a57170df008a055d7f71256e80343f707372ebca..0af3d1f86dc76db4b2e2627eedde8972e85a2338 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -177,7 +177,9 @@ def client_configurations(): """ Build configuration values for the client-side plugin. """ - return just({}) + return just({ + u"ristretto-issuer-root-url": u"https://issuer.example.invalid/", + }) def vouchers(): diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index b37cdbf6692dadb2657db85a4469703c4642cce5..d497c724877505caf6ae95cbc451e429fbb2d384 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 io import ( + BytesIO, +) + from zope.interface import ( implementer, ) @@ -36,6 +40,9 @@ from testtools.matchers import ( from testtools.twistedsupport import ( succeeded, ) +from testtools.content import ( + text_content, +) from hypothesis import ( given, ) @@ -76,6 +83,9 @@ from twisted.plugins.zkapauthorizer import ( from ..model import ( VoucherStore, ) +from ..controller import ( + IssuerConfigurationMismatch, +) from .strategies import ( minimal_tahoe_configs, @@ -227,6 +237,9 @@ tahoe_configs_with_dummy_redeemer = minimal_tahoe_configs({ u"privatestorageio-zkapauthz-v1": just({u"redeemer": u"dummy"}), }) +tahoe_configs_with_mismatched_issuer = minimal_tahoe_configs({ + u"privatestorageio-zkapauthz-v1": just({u"ristretto-issuer-root-url": u"https://another-issuer.example.invalid/"}), +}) class ClientPluginTests(TestCase): """ @@ -257,6 +270,31 @@ class ClientPluginTests(TestCase): ) + @given(tahoe_configs_with_mismatched_issuer, announcements()) + def test_mismatched_ristretto_issuer(self, get_config, announcement): + """ + ``get_storage_client`` raises an exception when called with an + announcement and local configuration which specify different issuers. + """ + tempdir = self.useFixture(TempDir()) + node_config = get_config( + tempdir.join(b"node"), + b"tub.port", + ) + config_text = BytesIO() + node_config.config.write(config_text) + self.addDetail(u"config", text_content(config_text.getvalue())) + self.addDetail(u"announcement", text_content(unicode(announcement))) + try: + result = storage_server.get_storage_client(node_config, announcement, get_rref) + except IssuerConfigurationMismatch: + pass + except Exception as e: + self.fail("get_storage_client raised the wrong exception: {}".format(e)) + else: + self.fail("get_storage_client didn't raise, returned: {}".format(result)) + + @given( tahoe_configs_with_dummy_redeemer, announcements(),