From ed05881cc233a83f0277ec83838f0c1d5e98493a Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Wed, 18 Sep 2019 09:42:10 -0400
Subject: [PATCH] Change "passes" to "unblinded tokens" in many places

Passes cannot be constructed until we know how we're going to use them since
they are derived from the unblinded tokens and an operation-specific message.
---
 src/_zkapauthorizer/_plugin.py               |  5 +-
 src/_zkapauthorizer/controller.py            | 35 ++++++-----
 src/_zkapauthorizer/model.py                 | 65 +++++++++++++-------
 src/_zkapauthorizer/tests/strategies.py      | 17 +++++
 src/_zkapauthorizer/tests/test_controller.py | 13 ++--
 src/_zkapauthorizer/tests/test_model.py      | 33 +++++-----
 src/_zkapauthorizer/tests/test_plugin.py     | 16 ++---
 7 files changed, 110 insertions(+), 74 deletions(-)

diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py
index 8b16a77..1584a19 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 285e721..844a06e 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 c2e5606..76a659d 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 8f6e74e..f35371c 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 636a7e3..f30eeaf 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 1a6ac3c..f614c1c 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 2587904..8c0f648 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([]),
-- 
GitLab