From b08e5e2cc5a4f149291a131e2a7e08756d1cf3f4 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Mon, 2 Mar 2020 15:16:00 -0500
Subject: [PATCH] Pass is constructed from preimage and signature

each, 88 bytes of base64
---
 src/_zkapauthorizer/_storage_client.py        |  2 +-
 src/_zkapauthorizer/controller.py             | 25 ++++++++----
 src/_zkapauthorizer/model.py                  | 24 ++++++++++--
 src/_zkapauthorizer/tests/strategies.py       | 39 ++++++++++++++++---
 src/_zkapauthorizer/tests/test_controller.py  |  2 +-
 .../tests/test_storage_protocol.py            |  2 +-
 .../tests/test_storage_server.py              |  6 ++-
 7 files changed, 80 insertions(+), 20 deletions(-)

diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py
index 579312e..182a739 100644
--- a/src/_zkapauthorizer/_storage_client.py
+++ b/src/_zkapauthorizer/_storage_client.py
@@ -85,7 +85,7 @@ class ZKAPAuthorizerStorageClient(object):
         """
         assert isinstance(message, unicode)
         return list(
-            t.text.encode("ascii")
+            t.pass_text.encode("ascii")
             for t
             in self._get_passes(message.encode("utf-8"), count)
         )
diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 32a52e9..5539cea 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -315,8 +315,18 @@ class DummyRedeemer(object):
         )
 
     def tokens_to_passes(self, message, unblinded_tokens):
+        def token_to_pass(token):
+            # Smear the unblinded token value across the two new values we
+            # need.
+            bs = b64decode(token.unblinded_token.encode("ascii"))
+            preimage = bs[:48] + b"x" * 16
+            signature = bs[48:] + b"y" * 16
+            return Pass(
+                b64encode(preimage).decode("ascii"),
+                b64encode(signature).decode("ascii"),
+            )
         return list(
-            Pass(token.unblinded_token)
+            token_to_pass(token)
             for token
             in unblinded_tokens
         )
@@ -487,16 +497,15 @@ class RistrettoRedeemer(object):
             for token
             in unblinded_tokens
         )
-        marshaled_passes = list(
-            preimage.encode_base64() + b" " + signature.encode_base64()
+        passes = list(
+            Pass(
+                preimage.encode_base64().decode("ascii"),
+                signature.encode_base64().decode("ascii"),
+            )
             for (preimage, signature)
             in zip(clients_preimages, clients_signatures)
         )
-        return list(
-            Pass(p.decode("ascii"))
-            for p
-            in marshaled_passes
-        )
+        return passes
 
 
 @attr.s
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index 7b309a3..114715b 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -665,14 +665,32 @@ 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
+    :ivar unicode pass_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(validator=attr.validators.instance_of(unicode))
+    preimage = attr.ib(
+        validator=attr.validators.and_(
+            attr.validators.instance_of(unicode),
+            is_base64_encoded(),
+            has_length(88),
+        ),
+    )
+
+    signature = attr.ib(
+        validator=attr.validators.and_(
+            attr.validators.instance_of(unicode),
+            is_base64_encoded(),
+            has_length(88),
+        ),
+    )
+
+    @property
+    def pass_text(self):
+        return u"{} {}".format(self.preimage, self.signature)
 
 
 @attr.s(frozen=True)
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index d62b840..a4f4ead 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -81,10 +81,14 @@ from ..model import (
 # Sizes informed by
 # https://github.com/brave-intl/challenge-bypass-ristretto/blob/2f98b057d7f353c12b2b12d0f5ae9ad115f1d0ba/src/oprf.rs#L18-L33
 
+# The length of a `TokenPreimage`, in bytes.
+_TOKEN_PREIMAGE_LENGTH = 64
 # The length of a `Token`, in bytes.
 _TOKEN_LENGTH = 96
 # The length of a `UnblindedToken`, in bytes.
 _UNBLINDED_TOKEN_LENGTH = 96
+# The length of a `VerificationSignature`, in bytes.
+_VERIFICATION_SIGNATURE_LENGTH = 64
 
 def _merge_dictionaries(dictionaries):
     result = {}
@@ -339,16 +343,41 @@ def random_tokens():
     )
 
 
+def token_preimages():
+    """
+    Build ``unicode`` strings representing base64-encoded token preimages.
+    """
+    return byte_strings(
+        label=b"token-preimage",
+        length=_TOKEN_PREIMAGE_LENGTH,
+        entropy=4,
+    ).map(
+        lambda bs: b64encode(bs).decode("ascii"),
+    )
+
+
+def verification_signatures():
+    """
+    Build ``unicode`` strings representing base64-encoded verification
+    signatures.
+    """
+    return byte_strings(
+        label=b"verification-signature",
+        length=_VERIFICATION_SIGNATURE_LENGTH,
+        entropy=4,
+    ).map(
+        lambda bs: b64encode(bs).decode("ascii"),
+    )
+
+
 def zkaps():
     """
     Build random ZKAPs as ``Pass` instances.
     """
     return builds(
-        lambda preimage, signature: Pass(u"{} {}".format(preimage, signature)),
-        # Sizes informed by
-        # https://github.com/brave-intl/challenge-bypass-ristretto/blob/2f98b057d7f353c12b2b12d0f5ae9ad115f1d0ba/src/oprf.rs#L18-L33
-        preimage=binary(min_size=64, max_size=64).map(urlsafe_b64encode),
-        signature=binary(min_size=64, max_size=64).map(urlsafe_b64encode),
+        Pass,
+        preimage=token_preimages(),
+        signature=verification_signatures(),
     )
 
 
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 5485b16..dec0176 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -469,7 +469,7 @@ def ristretto_verify(signing_key, message, marshaled_passes):
             VerificationSignature.decode_base64(s.encode("ascii")),
         )
     servers_passes = list(
-        decode(marshaled_pass.text)
+        decode(marshaled_pass.pass_text)
         for marshaled_pass
         in marshaled_passes
     )
diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py
index d595e7c..faa4f8f 100644
--- a/src/_zkapauthorizer/tests/test_storage_protocol.py
+++ b/src/_zkapauthorizer/tests/test_storage_protocol.py
@@ -211,7 +211,7 @@ class ShareTests(TestCase):
         def get_passes(message, count):
             self.spent_passes += count
             return list(
-                Pass(pass_.decode("ascii"))
+                Pass(*pass_.split(u" "))
                 for pass_
                 in make_passes(
                     self.signing_key,
diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py
index 8d28db8..4cef248 100644
--- a/src/_zkapauthorizer/tests/test_storage_server.py
+++ b/src/_zkapauthorizer/tests/test_storage_server.py
@@ -127,7 +127,11 @@ class PassValidationTests(TestCase):
             message,
             list(RandomToken.create() for i in range(valid_count)),
         )
-        all_passes = valid_passes + list(pass_.text.encode("ascii") for pass_ in invalid_passes)
+        all_passes = valid_passes + list(
+            pass_.pass_text.encode("ascii")
+            for pass_
+            in invalid_passes
+        )
         shuffle(all_passes)
 
         self.assertThat(
-- 
GitLab