diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index 844a06ed8ec681514de688be710fc08c0a801978..f415824d732f77c09b4afd7fc26d2b348053adb4 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -222,6 +222,11 @@ class RistrettoRedeemer(object):
     def tokens_to_passes(self, message, unblinded_tokens):
         # XXX Here's some more of the privacypass dance.  Something needs to
         # know to call this, I guess?  Also it's untested as heck.
+        unblinded_tokens = list(
+            privacypass.UnblindedToken.decode_base64(token.text.encode("ascii"))
+            for token
+            in unblinded_tokens
+        )
         clients_preimages = list(
             token.preimage()
             for token
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index f30eeafe409232efa7da74433b49466e4870a3b0..e7e7f92e89003f11c0dcb02f74aaa2660d4920ee 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -20,6 +20,9 @@ from json import (
     loads,
     dumps,
 )
+from functools import (
+    partial,
+)
 from zope.interface import (
     implementer,
 )
@@ -73,6 +76,8 @@ from privacypass import (
     PublicKey,
     BlindedToken,
     BatchDLEQProof,
+    TokenPreimage,
+    VerificationSignature,
     random_signing_key,
 )
 
@@ -205,7 +210,10 @@ class RistrettoRedeemerTests(TestCase):
         """
         signing_key = random_signing_key()
         issuer = RistrettoRedemption(signing_key)
-        # Make it lie about the public key it is using.
+
+        # Make it lie about the public key it is using.  This causes the proof
+        # to be invalid since it proves the signature was made with a
+        # different key than reported in the response.
         issuer.public_key = PublicKey.from_signing_key(random_signing_key())
 
         treq = treq_for_loopback_ristretto(issuer)
@@ -226,6 +234,75 @@ class RistrettoRedeemerTests(TestCase):
             ),
         )
 
+    @given(vouchers().map(Voucher), integers(min_value=1, max_value=100))
+    def test_ristretto_pass_construction(self, voucher, num_tokens):
+        """
+        The passes constructed using unblinded tokens and messages pass the
+        Ristretto verification check.
+        """
+        message = b"hello world"
+
+        signing_key = random_signing_key()
+        issuer = RistrettoRedemption(signing_key)
+        treq = treq_for_loopback_ristretto(issuer)
+        redeemer = RistrettoRedeemer(treq, NOWHERE)
+
+        random_tokens = redeemer.random_tokens_for_voucher(voucher, num_tokens)
+        d = redeemer.redeem(
+            voucher,
+            random_tokens,
+        )
+        def unblinded_tokens_to_passes(unblinded_tokens):
+            passes = redeemer.tokens_to_passes(message, unblinded_tokens)
+            return passes
+        d.addCallback(unblinded_tokens_to_passes)
+
+        self.assertThat(
+            d,
+            succeeded(
+                AfterPreprocessing(
+                    partial(ristretto_verify, signing_key, message),
+                    Equals(True),
+                ),
+            ),
+        )
+
+
+def ristretto_verify(signing_key, message, marshaled_passes):
+    servers_passes = list(
+        (
+            TokenPreimage.decode_base64(token_preimage),
+            VerificationSignature.decode_base64(sig),
+        )
+        for (token_preimage, sig)
+        in marshaled_passes
+    )
+    servers_unblinded_tokens = list(
+        signing_key.rederive_unblinded_token(token_preimage)
+        for (token_preimage, sig)
+        in servers_passes
+    )
+    servers_verification_sigs = list(
+        sig
+        for (token_preimage, sig)
+        in servers_passes
+    )
+    servers_verification_keys = list(
+        unblinded_token.derive_verification_key_sha512()
+        for unblinded_token
+        in servers_unblinded_tokens
+    )
+    invalid_passes = list(
+        key.invalid_sha512(
+            sig,
+            message,
+        )
+        for (key, sig)
+        in zip(servers_verification_keys, servers_verification_sigs)
+    )
+
+    return not any(invalid_passes)
+
 
 def treq_for_loopback_ristretto(local_issuer):
     """