From 9dc5f7fc180fbec7c5488ac28385506995f78a1b Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone <exarkun@twistedmatrix.com> Date: Thu, 15 Aug 2019 13:23:39 -0400 Subject: [PATCH] Round-trip ZKAPs through the data store. --- src/_zkapauthorizer/model.py | 79 +++++++++++++++++++++++++ src/_zkapauthorizer/tests/strategies.py | 18 ++++++ src/_zkapauthorizer/tests/test_model.py | 27 ++++++++- 3 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py index 58357cb..ec534fd 100644 --- a/src/_zkapauthorizer/model.py +++ b/src/_zkapauthorizer/model.py @@ -117,6 +117,15 @@ 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. + + PRIMARY KEY([text]) + ) + """, + ) return conn @@ -201,6 +210,76 @@ class VoucherStore(object): in refs ) + @with_cursor + def insert_passes(self, cursor, passes): + """ + Store some passes. + + :param list[Pass] passes: The passes to store. + """ + cursor.executemany( + """ + INSERT INTO [passes] VALUES (?) + """, + list((p.text,) for p in passes), + ) + + @with_cursor + def extract_passes(self, cursor, count): + """ + Remove and return some passes. + + :param int count: The maximum number of passes to remove and return. + If fewer passes than this are available, only as many as are + available are returned. + + :return list[Pass]: The removed passes. + """ + cursor.execute( + """ + CREATE TEMPORARY TABLE [extracting-passes] + AS + SELECT [text] FROM [passes] LIMIT ? + """, + (count,), + ) + cursor.execute( + """ + DELETE FROM [passes] WHERE [text] IN [extracting-passes] + """, + ) + cursor.execute( + """ + SELECT ([text]) FROM [extracting-passes] + """, + ) + texts = cursor.fetchall() + cursor.execute( + """ + DROP TABLE [extracting-passes] + """, + ) + return list( + Pass(t) + for (t,) + in texts + ) + + +@attr.s(frozen=True) +class Pass(object): + """ + A ``Pass`` instance completely represents a single Zero-Knowledge Access Pass. + + :ivar unicode text: The text value of the pass. This can be sent to a + service provider one time to anonymously prove a prior voucher + redemption. If it is sent more than once the service provider may + choose to reject it and the anonymity property is compromised. Pass + text should be kept secret. If pass text is divulged to third-parties + the anonymity property may be compromised. + """ + text = attr.ib(type=unicode) + @attr.s class Voucher(object): diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py index e615137..3b6fc63 100644 --- a/src/_zkapauthorizer/tests/strategies.py +++ b/src/_zkapauthorizer/tests/strategies.py @@ -52,6 +52,10 @@ from allmydata.client import ( config_from_string, ) +from ..model import ( + Pass, +) + def _merge_dictionaries(dictionaries): result = {} @@ -175,6 +179,20 @@ def vouchers(): ) +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")), + ) + + def request_paths(): """ Build lists of unicode strings that represent the path component of an diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py index 7732e5e..11d7304 100644 --- a/src/_zkapauthorizer/tests/test_model.py +++ b/src/_zkapauthorizer/tests/test_model.py @@ -64,6 +64,7 @@ from ..model import ( from .strategies import ( tahoe_configs, vouchers, + zkaps, ) @@ -146,7 +147,7 @@ class VoucherStoreTests(TestCase): ) - @given(tahoe_configs(), lists(vouchers())) + @given(tahoe_configs(), lists(vouchers(), unique=True)) def test_list(self, get_config, vouchers): """ ``VoucherStore.list`` returns a ``list`` containing a ``Voucher`` object @@ -250,3 +251,27 @@ class VoucherTests(TestCase): Voucher.from_json(ref.to_json()), Equals(ref), ) + + +class ZKAPStoreTests(TestCase): + """ + Tests for ZKAP-related functionality of ``VoucherStore``. + """ + @given(tahoe_configs(), lists(zkaps(), unique=True)) + def test_zkaps_round_trip(self, get_config, passes): + """ + ZKAPs that are added to the store can later be retrieved. + """ + tempdir = self.useFixture(TempDir()) + config = get_config(tempdir.join(b"node"), b"tub.port") + store = VoucherStore.from_node_config( + config, + memory_connect, + ) + store.insert_passes(passes) + retrieved_passes = store.extract_passes(len(passes)) + self.expectThat(passes, Equals(retrieved_passes)) + + # After extraction, the passes are no longer available. + more_passes = store.extract_passes(1) + self.expectThat([], Equals(more_passes)) -- GitLab