diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py
index 622299f6bd171f8905433aa4b48550e643efdca4..5f4e5685c896fcd4ed7bbc262b5c07780179529b 100644
--- a/src/_zkapauthorizer/_storage_client.py
+++ b/src/_zkapauthorizer/_storage_client.py
@@ -215,7 +215,7 @@ def _encode_passes(group):
 
     :return list[bytes]: The encoded form of the passes in the given group.
     """
-    return list(t.pass_text.encode("ascii") for t in group.passes)
+    return list(t.pass_bytes for t in group.passes)
 
 
 @implementer(IStorageServer)
diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py
index 1b541131867dab2371222a366dc38fe44d6d9c92..b8474998668a17b4da72d91780e0fe35ec45befa 100644
--- a/src/_zkapauthorizer/_storage_server.py
+++ b/src/_zkapauthorizer/_storage_server.py
@@ -49,6 +49,7 @@ from twisted.python.reflect import namedAny
 from zope.interface import implementer
 
 from .foolscap import RIPrivacyPassAuthorizedStorageServer, ShareStat
+from .model import Pass
 from .storage_common import (
     MorePassesRequired,
     add_lease_message,
@@ -112,9 +113,11 @@ class _ValidationResult(object):
         assert isinstance(message, unicode), "message %r not unicode" % (message,)
         assert isinstance(pass_, bytes), "pass %r not bytes" % (pass_,)
         try:
-            preimage_base64, signature_base64 = pass_.split(b" ")
-            preimage = TokenPreimage.decode_base64(preimage_base64)
-            proposed_signature = VerificationSignature.decode_base64(signature_base64)
+            parsed_pass = Pass.from_bytes(pass_)
+            preimage = TokenPreimage.decode_base64(parsed_pass.preimage)
+            proposed_signature = VerificationSignature.decode_base64(
+                parsed_pass.signature
+            )
             unblinded_token = signing_key.rederive_unblinded_token(preimage)
             verification_key = unblinded_token.derive_verification_key_sha512()
             invalid_pass = verification_key.invalid_sha512(
diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index edda44e5b90b03bccdc08d7667e2300824a85956..ab52c5bc98a8eec0d253b364945effe31f396ce0 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -318,7 +318,7 @@ class RecordingRedeemer(object):
 
 
 def dummy_random_tokens(voucher, counter, count):
-    v = urlsafe_b64decode(voucher.number.encode("ascii"))
+    v = urlsafe_b64decode(voucher.number)
 
     def dummy_random_token(n):
         return RandomToken(
@@ -326,7 +326,7 @@ def dummy_random_tokens(voucher, counter, count):
             # length) - 4 (fixed-width counter)
             b64encode(
                 v + u"{:0>4}{:0>60}".format(counter, n).encode("ascii"),
-            ).decode("ascii"),
+            ),
         )
 
     return list(dummy_random_token(n) for n in range(count))
@@ -380,9 +380,9 @@ class DummyRedeemer(object):
             )
 
         def dummy_unblinded_token(random_token):
-            random_value = b64decode(random_token.token_value.encode("ascii"))
+            random_value = b64decode(random_token.token_value)
             unblinded_value = random_value + b"x" * (96 - len(random_value))
-            return UnblindedToken(b64encode(unblinded_value).decode("ascii"))
+            return UnblindedToken(b64encode(unblinded_value))
 
         return succeed(
             RedemptionResult(
@@ -397,17 +397,13 @@ class DummyRedeemer(object):
             # can include in the resulting Pass.  This ensures the pass values
             # will be unique if and only if the unblinded tokens were unique
             # (barring improbable hash collisions).
-            token_digest = (
-                sha256(token.unblinded_token.encode("ascii"))
-                .hexdigest()
-                .encode("ascii")
-            )
+            token_digest = sha256(token.unblinded_token).hexdigest().encode("ascii")
 
             preimage = b"preimage-" + token_digest[len(b"preimage-") :]
             signature = b"signature-" + token_digest[len(b"signature-") :]
             return Pass(
-                b64encode(preimage).decode("ascii"),
-                b64encode(signature).decode("ascii"),
+                b64encode(preimage),
+                b64encode(signature),
             )
 
         return list(token_to_pass(token) for token in unblinded_tokens)
@@ -489,9 +485,7 @@ class RistrettoRedeemer(object):
     def random_tokens_for_voucher(self, voucher, counter, count):
         return list(
             RandomToken(
-                challenge_bypass_ristretto.RandomToken.create()
-                .encode_base64()
-                .decode("ascii"),
+                challenge_bypass_ristretto.RandomToken.create().encode_base64(),
             )
             for n in range(count)
         )
@@ -499,9 +493,7 @@ class RistrettoRedeemer(object):
     @inlineCallbacks
     def redeemWithCounter(self, voucher, counter, encoded_random_tokens):
         random_tokens = list(
-            challenge_bypass_ristretto.RandomToken.decode_base64(
-                token.token_value.encode("ascii")
-            )
+            challenge_bypass_ristretto.RandomToken.decode_base64(token.token_value)
             for token in encoded_random_tokens
         )
         blinded_tokens = list(token.blind() for token in random_tokens)
@@ -509,7 +501,7 @@ class RistrettoRedeemer(object):
             self._api_root.child(u"v1", u"redeem").to_text(),
             dumps(
                 {
-                    u"redeemVoucher": voucher.number,
+                    u"redeemVoucher": voucher.number.decode("ascii"),
                     u"redeemCounter": counter,
                     u"redeemTokens": list(
                         token.encode_base64() for token in blinded_tokens
@@ -570,8 +562,7 @@ class RistrettoRedeemer(object):
             )
         self._log.info("Validated proof")
         unblinded_tokens = list(
-            UnblindedToken(token.encode_base64().decode("ascii"))
-            for token in clients_unblinded_tokens
+            UnblindedToken(token.encode_base64()) for token in clients_unblinded_tokens
         )
         returnValue(
             RedemptionResult(
@@ -586,7 +577,7 @@ class RistrettoRedeemer(object):
         assert all(isinstance(element, UnblindedToken) for element in unblinded_tokens)
         unblinded_tokens = list(
             challenge_bypass_ristretto.UnblindedToken.decode_base64(
-                token.unblinded_token.encode("ascii")
+                token.unblinded_token
             )
             for token in unblinded_tokens
         )
@@ -600,8 +591,8 @@ class RistrettoRedeemer(object):
         clients_preimages = list(token.preimage() for token in unblinded_tokens)
         passes = list(
             Pass(
-                preimage.encode_base64().decode("ascii"),
-                signature.encode_base64().decode("ascii"),
+                preimage.encode_base64(),
+                signature.encode_base64(),
             )
             for (preimage, signature) in zip(clients_preimages, clients_signatures)
         )
@@ -805,7 +796,7 @@ class PaymentController(object):
     @inlineCallbacks
     def redeem(self, voucher, num_tokens=None):
         """
-        :param unicode voucher: A voucher to redeem.
+        :param bytes voucher: A voucher to redeem.
 
         :param int num_tokens: A number of tokens to redeem.
         """
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index 27e2fe947ebbf01d329022d72353c6b072aedca8..7530d2b2613538fdbe26f5a3e05b6a1b8d35eacf 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -247,7 +247,7 @@ class VoucherStore(object):
     @with_cursor
     def get(self, cursor, voucher):
         """
-        :param unicode voucher: The text value of a voucher to retrieve.
+        :param bytes voucher: The text value of a voucher to retrieve.
 
         :return Voucher: The voucher object that matches the given value.
         """
@@ -260,7 +260,7 @@ class VoucherStore(object):
             WHERE
                 [number] = ?
             """,
-            (voucher,),
+            (voucher.decode("ascii"),),
         )
         refs = cursor.fetchall()
         if len(refs) == 0:
@@ -274,7 +274,7 @@ class VoucherStore(object):
         existing) to the database.  If the (voucher, counter) pair is already
         present, do nothing.
 
-        :param unicode voucher: The text value of a voucher with which to
+        :param bytes voucher: The text value of a voucher with which to
             associate the tokens.
 
         :param int expected_tokens: The total number of tokens for which this
@@ -302,7 +302,7 @@ class VoucherStore(object):
             FROM [tokens]
             WHERE [voucher] = ? AND [counter] = ?
             """,
-            (voucher, counter),
+            (voucher.decode("ascii"), counter),
         )
         rows = cursor.fetchall()
         if len(rows) > 0:
@@ -312,26 +312,35 @@ class VoucherStore(object):
                 voucher=voucher,
                 counter=counter,
             )
-            tokens = list(RandomToken(token_value) for (token_value,) in rows)
+            tokens = list(
+                RandomToken(token_value.encode("ascii")) for (token_value,) in rows
+            )
         else:
             tokens = get_tokens()
             self._log.info(
                 "Persisting {count} random tokens for a voucher ({voucher}[{counter}]).",
                 count=len(tokens),
-                voucher=voucher,
+                voucher=voucher.decode("ascii"),
                 counter=counter,
             )
             cursor.execute(
                 """
                 INSERT OR IGNORE INTO [vouchers] ([number], [expected-tokens], [created]) VALUES (?, ?, ?)
                 """,
-                (voucher, expected_tokens, self.now()),
+                (voucher.decode("ascii"), expected_tokens, self.now()),
             )
             cursor.executemany(
                 """
                 INSERT INTO [tokens] ([voucher], [counter], [text]) VALUES (?, ?, ?)
                 """,
-                list((voucher, counter, token.token_value) for token in tokens),
+                list(
+                    (
+                        voucher.decode("ascii"),
+                        counter,
+                        token.token_value.decode("ascii"),
+                    )
+                    for token in tokens
+                ),
             )
         return tokens
 
@@ -385,7 +394,7 @@ class VoucherStore(object):
         """
         Store some unblinded tokens received from redemption of a voucher.
 
-        :param unicode voucher: The voucher associated with the unblinded
+        :param bytes 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.
 
@@ -417,7 +426,7 @@ class VoucherStore(object):
             """
             INSERT INTO [redemption-groups] ([voucher], [public-key], [spendable]) VALUES (?, ?, ?)
             """,
-            (voucher, public_key, spendable),
+            (voucher.decode("ascii"), public_key, spendable),
         )
         group_id = cursor.lastrowid
 
@@ -443,7 +452,7 @@ class VoucherStore(object):
                 token_count_increase,
                 sequestered_count_increase,
                 self.now(),
-                voucher,
+                voucher.decode("ascii"),
             ),
         )
         if cursor.rowcount == 0:
@@ -453,7 +462,7 @@ class VoucherStore(object):
 
         self._insert_unblinded_tokens(
             cursor,
-            list(t.unblinded_token for t in unblinded_tokens),
+            list(t.unblinded_token.decode("ascii") for t in unblinded_tokens),
             group_id,
         )
 
@@ -471,7 +480,7 @@ class VoucherStore(object):
             WHERE [number] = ?
               AND [state] = "pending"
             """,
-            (self.now(), voucher),
+            (self.now(), voucher.decode("ascii")),
         )
         if cursor.rowcount == 0:
             # Was there no matching voucher or was it in the wrong state?
@@ -481,7 +490,7 @@ class VoucherStore(object):
                 FROM [vouchers]
                 WHERE [number] = ?
                 """,
-                (voucher,),
+                (voucher.decode("ascii"),),
             )
             rows = cursor.fetchall()
             if len(rows) == 0:
@@ -541,7 +550,7 @@ class VoucherStore(object):
             """,
             texts,
         )
-        return list(UnblindedToken(t) for (t,) in texts)
+        return list(UnblindedToken(t.encode("ascii")) for (t,) in texts)
 
     @with_cursor
     def count_unblinded_tokens(self, cursor):
@@ -577,7 +586,9 @@ class VoucherStore(object):
             """
             INSERT INTO [to-discard] VALUES (?)
             """,
-            list((token.unblinded_token,) for token in unblinded_tokens),
+            list(
+                (token.unblinded_token.decode("ascii"),) for token in unblinded_tokens
+            ),
         )
         cursor.execute(
             """
@@ -614,7 +625,10 @@ class VoucherStore(object):
             """
             INSERT INTO [invalid-unblinded-tokens] VALUES (?, ?)
             """,
-            list((token.unblinded_token, reason) for token in unblinded_tokens),
+            list(
+                (token.unblinded_token.decode("ascii"), reason)
+                for token in unblinded_tokens
+            ),
         )
         cursor.execute(
             """
@@ -640,7 +654,9 @@ class VoucherStore(object):
             """
             INSERT INTO [to-reset] VALUES (?)
             """,
-            list((token.unblinded_token,) for token in unblinded_tokens),
+            list(
+                (token.unblinded_token.decode("ascii"),) for token in unblinded_tokens
+            ),
         )
         cursor.execute(
             """
@@ -813,7 +829,7 @@ class UnblindedToken(object):
     and can be used to construct a privacy-preserving pass which can be
     exchanged for service.
 
-    :ivar unicode unblinded_token: The base64 encoded serialized form of the
+    :ivar bytes unblinded_token: The base64 encoded serialized form of the
         unblinded token.  This can be used to reconstruct a
         ``challenge_bypass_ristretto.UnblindedToken`` using that class's
         ``decode_base64`` method.
@@ -821,7 +837,7 @@ class UnblindedToken(object):
 
     unblinded_token = attr.ib(
         validator=attr.validators.and_(
-            attr.validators.instance_of(unicode),
+            attr.validators.instance_of(bytes),
             is_base64_encoded(),
             has_length(128),
         ),
@@ -833,7 +849,7 @@ class Pass(object):
     """
     A ``Pass`` instance completely represents a single Zero-Knowledge Access Pass.
 
-    :ivar unicode pass_text: The text value of the pass.  This can be sent to
+    :ivar bytes pass_bytes: 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
@@ -843,7 +859,7 @@ class Pass(object):
 
     preimage = attr.ib(
         validator=attr.validators.and_(
-            attr.validators.instance_of(unicode),
+            attr.validators.instance_of(bytes),
             is_base64_encoded(),
             has_length(88),
         ),
@@ -851,27 +867,31 @@ class Pass(object):
 
     signature = attr.ib(
         validator=attr.validators.and_(
-            attr.validators.instance_of(unicode),
+            attr.validators.instance_of(bytes),
             is_base64_encoded(),
             has_length(88),
         ),
     )
 
     @property
-    def pass_text(self):
-        return u"{} {}".format(self.preimage, self.signature)
+    def pass_bytes(self):
+        return b" ".join((self.preimage, self.signature))
+
+    @classmethod
+    def from_bytes(cls, pass_):
+        return cls(*pass_.split(b" "))
 
 
 @attr.s(frozen=True)
 class RandomToken(object):
     """
-    :ivar unicode token_value: The base64-encoded representation of the random
+    :ivar bytes token_value: The base64-encoded representation of the random
         token.
     """
 
     token_value = attr.ib(
         validator=attr.validators.and_(
-            attr.validators.instance_of(unicode),
+            attr.validators.instance_of(bytes),
             is_base64_encoded(),
             has_length(128),
         ),
@@ -1014,7 +1034,7 @@ class Error(object):
 @attr.s(frozen=True)
 class Voucher(object):
     """
-    :ivar unicode number: The text string which gives this voucher its
+    :ivar bytes number: The byte string which gives this voucher its
         identity.
 
     :ivar datetime created: The time at which this voucher was added to this
@@ -1032,7 +1052,7 @@ class Voucher(object):
 
     number = attr.ib(
         validator=attr.validators.and_(
-            attr.validators.instance_of(unicode),
+            attr.validators.instance_of(bytes),
             is_base64_encoded(urlsafe_b64decode),
             has_length(44),
         ),
@@ -1085,7 +1105,7 @@ class Voucher(object):
         number, created, expected_tokens, state = row[:4]
 
         return cls(
-            number=number,
+            number=number.encode("ascii"),
             expected_tokens=expected_tokens,
             # All Python datetime-based date/time libraries fail to handle
             # leap seconds.  This parse call might raise an exception of the
@@ -1135,7 +1155,7 @@ class Voucher(object):
             raise ValueError("Unrecognized state {!r}".format(state_json))
 
         return cls(
-            number=values[u"number"],
+            number=values[u"number"].encode("ascii"),
             expected_tokens=values[u"expected-tokens"],
             created=None
             if values[u"created"] is None
@@ -1152,7 +1172,7 @@ class Voucher(object):
     def to_json_v1(self):
         state = self.state.to_json_v1()
         return {
-            u"number": self.number,
+            u"number": self.number.decode("ascii"),
             u"expected-tokens": self.expected_tokens,
             u"created": None if self.created is None else self.created.isoformat(),
             u"state": state,
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index 4f955e2c46304260d40be28a091ced5e43223511..f5db0531f7d9563ddcee051475e0c9cf2d6ff1ba 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -415,7 +415,7 @@ class _VoucherCollection(Resource):
         self._log.info(
             "Accepting a voucher ({voucher}) for redemption.", voucher=voucher
         )
-        self._controller.redeem(voucher)
+        self._controller.redeem(voucher.encode("ascii"))
         return b""
 
     def render_GET(self, request):
@@ -434,7 +434,7 @@ class _VoucherCollection(Resource):
         if not is_syntactic_voucher(voucher):
             return bad_request()
         try:
-            voucher = self._store.get(voucher)
+            voucher = self._store.get(voucher.encode("ascii"))
         except KeyError:
             return NoResource()
         return VoucherView(self._controller.incorporate_transient_state(voucher))
diff --git a/src/_zkapauthorizer/tests/privacypass.py b/src/_zkapauthorizer/tests/privacypass.py
index a3820b0924ec3f6fa812141dd004c18599fbbc47..83c54949e648ef8a98d022b09e1a29b109124ac9 100644
--- a/src/_zkapauthorizer/tests/privacypass.py
+++ b/src/_zkapauthorizer/tests/privacypass.py
@@ -34,7 +34,7 @@ def make_passes(signing_key, for_message, random_tokens):
     :param list[challenge_bypass_ristretto.RandomToken] random_tokens: The
         random tokens to feed in to the pass generation process.
 
-    :return list[unicode]: The privacy passes.  The returned list has one
+    :return list[Pass]: The privacy passes.  The returned list has one
         element for each element of ``random_tokens``.
     """
     blinded_tokens = list(token.blind() for token in random_tokens)
@@ -64,10 +64,12 @@ def make_passes(signing_key, for_message, random_tokens):
         for verification_key in verification_keys
     )
     passes = list(
-        u"{} {}".format(
-            preimage.encode_base64().decode("ascii"),
-            signature.encode_base64().decode("ascii"),
-        ).encode("ascii")
+        b" ".join(
+            (
+                preimage.encode_base64(),
+                signature.encode_base64(),
+            )
+        )
         for (preimage, signature) in zip(preimages, message_signatures)
     )
     return passes
diff --git a/src/_zkapauthorizer/tests/storage_common.py b/src/_zkapauthorizer/tests/storage_common.py
index c28cff6a4aceee2f6059e3fae735b0926b0bb4f6..a72cbb5f9eab5757e8828da08e3df524c74cda45 100644
--- a/src/_zkapauthorizer/tests/storage_common.py
+++ b/src/_zkapauthorizer/tests/storage_common.py
@@ -164,7 +164,7 @@ def get_passes(message, count, signing_key):
         and bound to the given message.
     """
     return list(
-        Pass(*pass_.split(u" "))
+        Pass.from_bytes(pass_)
         for pass_ in make_passes(
             signing_key,
             message,
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index dc8763acd552c258d9bab1db243a506b8c62f8b2..c439874b7d6b1560cc881138d4fcf8a50dc4beb7 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -548,19 +548,10 @@ def share_parameters():
 
 def vouchers():
     """
-    Build unicode strings in the format of vouchers.
+    Build byte strings in the format of vouchers.
     """
-    return (
-        binary(
-            min_size=32,
-            max_size=32,
-        )
-        .map(
-            urlsafe_b64encode,
-        )
-        .map(
-            lambda voucher: voucher.decode("ascii"),
-        )
+    return binary(min_size=32, max_size=32,).map(
+        urlsafe_b64encode,
     )
 
 
@@ -674,40 +665,32 @@ def random_tokens():
             length=_TOKEN_LENGTH,
             entropy=4,
         )
-        .map(
-            b64encode,
-        )
-        .map(
-            lambda token: RandomToken(token.decode("ascii")),
-        )
+        .map(b64encode)
+        .map(RandomToken)
     )
 
 
 def token_preimages():
     """
-    Build ``unicode`` strings representing base64-encoded token preimages.
+    Build ``bytes`` 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"),
-    )
+    ).map(b64encode)
 
 
 def verification_signatures():
     """
-    Build ``unicode`` strings representing base64-encoded verification
+    Build ``bytes`` 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"),
-    )
+    ).map(b64encode)
 
 
 def zkaps():
@@ -733,12 +716,8 @@ def unblinded_tokens():
             length=_UNBLINDED_TOKEN_LENGTH,
             entropy=4,
         )
-        .map(
-            b64encode,
-        )
-        .map(
-            lambda zkap: UnblindedToken(zkap.decode("ascii")),
-        )
+        .map(b64encode)
+        .map(UnblindedToken)
     )
 
 
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index b4e928c6d99231f90a79161561b97f7258a3ffd3..7e3e9abf1067d27a2c3a00b44bc94c1f47c9526a 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -493,7 +493,8 @@ class UnblindedTokenTests(TestCase):
             dumps(
                 {
                     u"unblinded-tokens": list(
-                        token.unblinded_token for token in unblinded_tokens
+                        token.unblinded_token.decode("ascii")
+                        for token in unblinded_tokens
                     )
                 }
             )
@@ -941,7 +942,7 @@ class VoucherTests(TestCase):
         )
         root = root_from_config(config, datetime.now)
         agent = RequestTraversalAgent(root)
-        data = BytesIO(dumps({u"voucher": voucher}))
+        data = BytesIO(dumps({u"voucher": voucher.decode("ascii")}))
         requesting = authorized_request(
             api_auth_token,
             agent,
@@ -1234,7 +1235,7 @@ class VoucherTests(TestCase):
             agent,
             b"PUT",
             b"http://127.0.0.1/voucher",
-            data=BytesIO(dumps({u"voucher": voucher})),
+            data=BytesIO(dumps({u"voucher": voucher.decode("ascii")})),
         )
         self.assertThat(
             putting,
@@ -1361,7 +1362,7 @@ class VoucherTests(TestCase):
         note("{} vouchers".format(len(vouchers)))
 
         for voucher in vouchers:
-            data = BytesIO(dumps({u"voucher": voucher}))
+            data = BytesIO(dumps({u"voucher": voucher.decode("ascii")}))
             putting = authorized_request(
                 api_auth_token,
                 agent,
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index f84bf0523ed8c51ee47aba8ba8397e3860aba97b..e4f08b1ae621d7ca86d81a6698a768106eb1a176 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -76,6 +76,7 @@ from ..controller import (
 )
 from ..model import DoubleSpend as model_DoubleSpend
 from ..model import Error as model_Error
+from ..model import Pass
 from ..model import Pending as model_Pending
 from ..model import Redeemed as model_Redeemed
 from ..model import Redeeming as model_Redeeming
@@ -1016,14 +1017,14 @@ def ristretto_verify(signing_key, message, marshaled_passes):
     """
 
     def decode(marshaled_pass):
-        t, s = marshaled_pass.split(u" ")
+        pass_ = Pass.from_bytes(marshaled_pass)
         return (
-            TokenPreimage.decode_base64(t.encode("ascii")),
-            VerificationSignature.decode_base64(s.encode("ascii")),
+            TokenPreimage.decode_base64(pass_.preimage),
+            VerificationSignature.decode_base64(pass_.signature),
         )
 
     servers_passes = list(
-        decode(marshaled_pass.pass_text) for marshaled_pass in marshaled_passes
+        decode(marshaled_pass.pass_bytes) for marshaled_pass in marshaled_passes
     )
     servers_unblinded_tokens = list(
         signing_key.rederive_unblinded_token(token_preimage)
diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py
index bff6a608515b62e430e8d7b3af69849f7d7f1365..93605cd83f2f373021707a0edb5cf7d49df81024 100644
--- a/src/_zkapauthorizer/tests/test_model.py
+++ b/src/_zkapauthorizer/tests/test_model.py
@@ -61,6 +61,7 @@ from ..model import (
     DoubleSpend,
     LeaseMaintenanceActivity,
     NotEnoughTokens,
+    Pass,
     Pending,
     Redeemed,
     StoreOpenError,
@@ -80,6 +81,7 @@ from .strategies import (
     voucher_counters,
     voucher_objects,
     vouchers,
+    zkaps,
 )
 
 
@@ -951,3 +953,19 @@ def store_for_test(testcase, get_config, get_now):
         memory_connect,
     )
     return store
+
+
+class PassTests(TestCase):
+    """
+    Tests for ``Pass``.
+    """
+
+    @given(zkaps())
+    def test_roundtrip(self, pass_):
+        """
+        ``Pass`` round-trips through ``Pass.from_bytes`` and ``Pass.pass_bytes``.
+        """
+        self.assertThat(
+            Pass.from_bytes(pass_.pass_bytes),
+            Equals(pass_),
+        )
diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py
index 82b62acbdab9f855efc5cd83503252f07627627a..bc112611969135e53c579b4ebb8b6b816d1cfa7f 100644
--- a/src/_zkapauthorizer/tests/test_storage_protocol.py
+++ b/src/_zkapauthorizer/tests/test_storage_protocol.py
@@ -256,7 +256,7 @@ class ShareTests(TestCase):
             # it.
             self.local_remote_server.callRemote(
                 "allocate_buckets",
-                list(pass_.pass_text.encode("ascii") for pass_ in all_passes),
+                list(pass_.pass_bytes for pass_ in all_passes),
                 storage_index,
                 renew_secret,
                 cancel_secret,
diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py
index ad570cb0205f092566c7773a2f04c60e74a215e6..3f5d55a4e71eb7e98b74f9661904842ca1f088c5 100644
--- a/src/_zkapauthorizer/tests/test_storage_server.py
+++ b/src/_zkapauthorizer/tests/test_storage_server.py
@@ -80,9 +80,7 @@ class ValidationResultTests(TestCase):
             message,
             list(RandomToken.create() for i in range(valid_count)),
         )
-        all_passes = valid_passes + list(
-            pass_.pass_text.encode("ascii") for pass_ in invalid_passes
-        )
+        all_passes = valid_passes + list(pass_.pass_bytes for pass_ in invalid_passes)
         shuffle(all_passes)
 
         self.assertThat(
diff --git a/src/_zkapauthorizer/validators.py b/src/_zkapauthorizer/validators.py
index 0b7284a54cc2894dddc41c426c88b2a4e843ea10..5466dd80963eae3d11157e68b7c4267a954b213e 100644
--- a/src/_zkapauthorizer/validators.py
+++ b/src/_zkapauthorizer/validators.py
@@ -20,12 +20,17 @@ from base64 import b64decode
 
 
 def is_base64_encoded(b64decode=b64decode):
+    """
+    Return a a attrs validator that verifies that the attributes is a base64
+    encoded byte string.
+    """
+
     def validate_is_base64_encoded(inst, attr, value):
         try:
-            b64decode(value.encode("ascii"))
+            b64decode(value)
         except TypeError:
             raise TypeError(
-                "{name!r} must be base64 encoded unicode, (got {value!r})".format(
+                "{name!r} must be base64 encoded bytes, (got {value!r})".format(
                     name=attr.name,
                     value=value,
                 ),