diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py index 8b16a7751eec14dc61e1751101839724440ea346..1584a19dc169c70a2ed4a53f8b2d09aafa58ad53 100644 --- a/src/_zkapauthorizer/_plugin.py +++ b/src/_zkapauthorizer/_plugin.py @@ -119,9 +119,8 @@ class ZKAPAuthorizer(object): """ return ZKAPAuthorizerStorageClient( get_rref, - # TODO: Make the caller figure out the correct number of - # passes to extract. - partial(self._get_store(node_config).extract_passes, 1), + # TODO: Make the caller figure out the correct number to extract. + partial(self._get_store(node_config).extract_unblinded_tokens, 1), ) diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py index 285e721b2542180eeeb64432b6b9220e59a26975..844a06ed8ec681514de688be710fc08c0a801978 100644 --- a/src/_zkapauthorizer/controller.py +++ b/src/_zkapauthorizer/controller.py @@ -45,13 +45,10 @@ from treq import ( import privacypass -from .foolscap import ( - TOKEN_LENGTH, -) from .model import ( RandomToken, + UnblindedToken, Voucher, - Pass, ) @@ -86,7 +83,8 @@ class IRedeemer(Interface): def redeem(voucher, random_tokens): """ - Redeem a voucher for passes. + Redeem a voucher for unblinded tokens which can be used to construct + passes. Implementations of this method do not need to be fault tolerant. If a redemption attempt is interrupted before it completes, it is the @@ -98,8 +96,8 @@ class IRedeemer(Interface): :param list[RandomToken] random_tokens: The random tokens to use in the redemption process. - :return: A ``Deferred`` which fires with a list of ``Pass`` instances - on successful redemption or which fails with + :return: A ``Deferred`` which fires with a list of ``UnblindedToken`` + instances on successful redemption or which fails with ``TransientRedemptionError`` on any error which may be resolved by simply trying again later or which fails with ``PermanentRedemptionError`` on any error which is definitive and @@ -147,12 +145,12 @@ class DummyRedeemer(object): def redeem(self, voucher, random_tokens): """ - :return: An already-fired ``Deferred`` that has a list of ``Pass`` - instances wrapping meaningless values. + :return: An already-fired ``Deferred`` that has a list of + ``UnblindedToken`` instances wrapping meaningless values. """ return succeed( list( - Pass((u"pass-" + token.token_value).zfill(TOKEN_LENGTH)) + UnblindedToken(token.token_value) for token in random_tokens ), @@ -216,8 +214,8 @@ class RistrettoRedeemer(object): public_key, ) returnValue(list( - Pass(text=unblinded_token.encode_base64().decode("ascii")) - for unblinded_token + UnblindedToken(token.encode_base64().decode("ascii")) + for token in clients_unblinded_tokens )) @@ -267,8 +265,10 @@ class PaymentController(object): 3. The controller hands the voucher and some random tokens to a redeemer. In the future, this step will need to be retried in the case of failures. - 4. When the voucher has been redeemed for passes, the controller hands them to the data store with the voucher. - The data store marks the voucher as redeemed and stores the passes for use by the storage client. + 4. When the voucher has been redeemed for unblinded tokens (inputs to + pass construction), the controller hands them to the data store with + the voucher. The data store marks the voucher as redeemed and stores + the unblinded tokens for use by the storage client. """ store = attr.ib() redeemer = attr.ib() @@ -297,9 +297,10 @@ class PaymentController(object): partial(self._redeemSuccess, voucher), ) - def _redeemSuccess(self, voucher, passes): + def _redeemSuccess(self, voucher, unblinded_tokens): """ Update the database state to reflect that a voucher was redeemed and to - store the resulting passes. + store the resulting unblinded tokens (which can be used to construct + passes later). """ - self.store.insert_passes_for_voucher(voucher, passes) + self.store.insert_unblinded_tokens_for_voucher(voucher, unblinded_tokens) diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index c2e56063489167c5e41ace8f3526751213e0a4ef..76a659da860617fd26dc39afaebe3b25be8cad3a 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -136,10 +136,10 @@ def open_and_initialize(path, required_schema_version, connect=None): ) cursor.execute( """ - CREATE TABLE IF NOT EXISTS [passes] ( - [text] text, -- The string that defines the pass. + CREATE TABLE IF NOT EXISTS [unblinded-tokens] ( + [token] text, -- The base64 encoded unblinded token. - PRIMARY KEY([text]) + PRIMARY KEY([token]) ) """, ) @@ -271,21 +271,26 @@ class VoucherStore(object): ) @with_cursor - def insert_passes_for_voucher(self, cursor, voucher, passes): + def insert_unblinded_tokens_for_voucher(self, cursor, voucher, unblinded_tokens): """ - Store some passes. + Store some unblinded tokens. - :param unicode voucher: The voucher associated with the passes. This - voucher will be marked as redeemed to indicate it has fulfilled - its purpose and has no further use for us. + :param unicode voucher: The voucher associated with the unblinded + tokens. This voucher will be marked as redeemed to indicate it + has fulfilled its purpose and has no further use for us. - :param list[Pass] passes: The passes to store. + :param list[UnblindedToken] unblinded_tokens: The unblinded tokens to + store. """ cursor.executemany( """ - INSERT INTO [passes] VALUES (?) + INSERT INTO [unblinded-tokens] VALUES (?) """, - list((p.text,) for p in passes), + list( + (t.text,) + for t + in unblinded_tokens + ), ) cursor.execute( """ @@ -295,47 +300,63 @@ class VoucherStore(object): ) @with_cursor - def extract_passes(self, cursor, count): + def extract_unblinded_tokens(self, cursor, count): """ - Remove and return some passes. + Remove and return some unblinded tokens. - :param int count: The maximum number of passes to remove and return. - If fewer passes than this are available, only as many as are + :param int count: The maximum number of unblinded tokens to remove and + return. If fewer than this are available, only as many as are available are returned. - :return list[Pass]: The removed passes. + :return list[UnblindedTokens]: The removed unblinded tokens. """ cursor.execute( """ - CREATE TEMPORARY TABLE [extracting-passes] + CREATE TEMPORARY TABLE [extracting] AS - SELECT [text] FROM [passes] LIMIT ? + SELECT [token] FROM [unblinded-tokens] LIMIT ? """, (count,), ) cursor.execute( """ - DELETE FROM [passes] WHERE [text] IN [extracting-passes] + DELETE FROM [unblinded-tokens] WHERE [token] IN [extracting] """, ) cursor.execute( """ - SELECT [text] FROM [extracting-passes] + SELECT [token] FROM [extracting] """, ) texts = cursor.fetchall() cursor.execute( """ - DROP TABLE [extracting-passes] + DROP TABLE [extracting] """, ) return list( - Pass(t) + UnblindedToken(t) for (t,) in texts ) +@attr.s(frozen=True) +class UnblindedToken(object): + """ + An ``UnblindedToken`` instance represents cryptographic proof of a voucher + redemption. It is an intermediate artifact in the PrivacyPass protocol + and can be used to construct a privacy-preserving pass which can be + exchanged for service. + + :ivar unicode text: The base64 encoded serialized form of the unblinded + token. This can be used to reconstruct a + ``privacypass.UnblindedToken`` using that class's ``decode_base64`` + method. + """ + text = attr.ib(validator=attr.validators.instance_of(unicode)) + + @attr.s(frozen=True) class Pass(object): """ diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index 8f6e74ed6c4a71cff35f75630b7b26fb3232c543..f35371cc1a4f904ddaeed4bfc71293a3930e8688 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -55,6 +55,7 @@ from allmydata.client import ( from ..model import ( Pass, RandomToken, + UnblindedToken ) @@ -208,6 +209,22 @@ def zkaps(): ) +def unblinded_tokens(): + """ + Builds random ``_zkapauthorizer.model.UnblindedToken`` wrapping invalid + base64 encode data. You cannot use these in the PrivacyPass cryptographic + protocol but you can put them into the database and take them out again. + """ + return binary( + min_size=32, + max_size=32, + ).map( + urlsafe_b64encode, + ).map( + lambda zkap: UnblindedToken(zkap.decode("ascii")), + ) + + def request_paths(): """ Build lists of unicode strings that represent the path component of an diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py index 636a7e33cb9ddf77e660e4f89004d4845bfdc96a..f30eeafe409232efa7da74433b49466e4870a3b0 100644 --- a/src/_zkapauthorizer/tests/test_controller.py +++ b/src/_zkapauthorizer/tests/test_controller.py @@ -23,7 +23,6 @@ from json import ( from zope.interface import ( implementer, ) -import attr from testtools import ( TestCase, ) @@ -58,11 +57,9 @@ from twisted.python.url import ( ) from twisted.internet.defer import ( fail, - succeed, ) from twisted.web.iweb import ( IAgent, - IBodyProducer, ) from twisted.web.resource import ( Resource, @@ -91,7 +88,7 @@ from ..model import ( memory_connect, VoucherStore, Voucher, - Pass, + UnblindedToken, ) from .strategies import ( @@ -176,7 +173,7 @@ class RistrettoRedeemerTests(TestCase): """ If the issuer returns a successful result then ``RistrettoRedeemer.redeem`` returns a ``Deferred`` that fires with a - list of ``Pass`` instances. + list of ``UnblindedToken`` instances. """ signing_key = random_signing_key() issuer = RistrettoRedemption(signing_key) @@ -192,7 +189,7 @@ class RistrettoRedeemerTests(TestCase): succeeded( MatchesAll( AllMatch( - IsInstance(Pass), + IsInstance(UnblindedToken), ), HasLength(num_tokens), ), @@ -202,9 +199,9 @@ class RistrettoRedeemerTests(TestCase): @given(vouchers().map(Voucher), integers(min_value=1, max_value=100)) def test_bad_ristretto_redemption(self, voucher, num_tokens): """ - If the issuer returns a successful result then + If the issuer returns a successful result with an invalid proof then ``RistrettoRedeemer.redeem`` returns a ``Deferred`` that fires with a - list of ``Pass`` instances. + ``Failure`` wrapping ``SecurityException``. """ signing_key = random_signing_key() issuer = RistrettoRedemption(signing_key) diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 1a6ac3cf944dc53b3b6ad554ea85addd170b9aaa..f614c1c912823cec16cf9db27a546c46d27a7b9b 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -65,7 +65,7 @@ from .strategies import ( tahoe_configs, vouchers, random_tokens, - zkaps, + unblinded_tokens, ) @@ -255,14 +255,14 @@ class VoucherTests(TestCase): ) -class ZKAPStoreTests(TestCase): +class UnblindedTokenStoreTests(TestCase): """ - Tests for ZKAP-related functionality of ``VoucherStore``. + Tests for ``UnblindedToken``-related functionality of ``VoucherStore``. """ - @given(tahoe_configs(), vouchers(), lists(zkaps(), unique=True)) - def test_zkaps_round_trip(self, get_config, voucher_value, passes): + @given(tahoe_configs(), vouchers(), lists(unblinded_tokens(), unique=True)) + def test_unblinded_tokens_round_trip(self, get_config, voucher_value, tokens): """ - ZKAPs that are added to the store can later be retrieved. + Unblinded tokens that are added to the store can later be retrieved. """ tempdir = self.useFixture(TempDir()) config = get_config(tempdir.join(b"node"), b"tub.port") @@ -270,18 +270,19 @@ class ZKAPStoreTests(TestCase): config, memory_connect, ) - store.insert_passes_for_voucher(voucher_value, passes) - retrieved_passes = store.extract_passes(len(passes)) - self.expectThat(passes, Equals(retrieved_passes)) + store.insert_unblinded_tokens_for_voucher(voucher_value, tokens) + retrieved_tokens = store.extract_unblinded_tokens(len(tokens)) + self.expectThat(tokens, Equals(retrieved_tokens)) - # After extraction, the passes are no longer available. - more_passes = store.extract_passes(1) - self.expectThat([], Equals(more_passes)) + # After extraction, the unblinded tokens are no longer available. + more_unblinded_tokens = store.extract_unblinded_tokens(1) + self.expectThat([], Equals(more_unblinded_tokens)) - @given(tahoe_configs(), vouchers(), random_tokens(), zkaps()) - def test_mark_vouchers_redeemed(self, get_config, voucher_value, token, one_pass): + @given(tahoe_configs(), vouchers(), random_tokens(), unblinded_tokens()) + def test_mark_vouchers_redeemed(self, get_config, voucher_value, token, one_token): """ - The voucher for ZKAPs that are added to the store are marked as redeemed. + The voucher for unblinded tokens that are added to the store is marked as + redeemed. """ tempdir = self.useFixture(TempDir()) config = get_config(tempdir.join(b"node"), b"tub.port") @@ -290,6 +291,6 @@ class ZKAPStoreTests(TestCase): memory_connect, ) store.add(voucher_value, [token]) - store.insert_passes_for_voucher(voucher_value, [one_pass]) + store.insert_unblinded_tokens_for_voucher(voucher_value, [one_token]) loaded_voucher = store.get(voucher_value) self.assertThat(loaded_voucher.redeemed, Equals(True)) diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py index 2587904e68bf78928d8b4344a653de7db4d4a4cd..8c0f648d77f7bf80c5fab8e5fe72d498faf9a1dd 100644 --- a/src/_zkapauthorizer/tests/test_plugin.py +++ b/src/_zkapauthorizer/tests/test_plugin.py @@ -82,7 +82,7 @@ from .strategies import ( announcements, vouchers, random_tokens, - zkaps, + unblinded_tokens, storage_indexes, lease_renew_secrets, ) @@ -256,23 +256,23 @@ class ClientPluginTests(TestCase): announcements(), vouchers(), random_tokens(), - zkaps(), + unblinded_tokens(), storage_indexes(), lease_renew_secrets(), ) - def test_passes_extracted( + def test_unblinded_tokens_extracted( self, get_config, announcement, voucher, token, - zkap, + unblinded_token, storage_index, renew_secret, ): """ The ``ZKAPAuthorizerStorageServer`` returned by ``get_storage_client`` - extracts passes from the plugin database. + extracts unblinded tokens from the plugin database. """ tempdir = self.useFixture(TempDir()) node_config = get_config( @@ -282,7 +282,7 @@ class ClientPluginTests(TestCase): store = VoucherStore.from_node_config(node_config) store.add(voucher, [token]) - store.insert_passes_for_voucher(voucher, [zkap]) + store.insert_unblinded_tokens_for_voucher(voucher, [unblinded_token]) storage_client = storage_server.get_storage_client( node_config, @@ -298,8 +298,8 @@ class ClientPluginTests(TestCase): ) d.addBoth(lambda ignored: None) - # There should be no passes left to extract. - remaining = store.extract_passes(1) + # There should be no unblinded tokens left to extract. + remaining = store.extract_unblinded_tokens(1) self.assertThat( remaining, Equals([]),