diff --git a/src/_zkapauthorizer/_json.py b/src/_zkapauthorizer/_json.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff8c0c2070e275b3761038c868540552b8c11146
--- /dev/null
+++ b/src/_zkapauthorizer/_json.py
@@ -0,0 +1,34 @@
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from future.utils import PY2
+
+if PY2:
+    from future.builtins import (  # noqa: F401
+        filter,
+        map,
+        zip,
+        ascii,
+        chr,
+        hex,
+        input,
+        next,
+        oct,
+        open,
+        pow,
+        round,
+        super,
+        bytes,
+        dict,
+        list,
+        object,
+        range,
+        str,
+        max,
+        min,
+    )
+
+from six import ensure_binary
+from json import dumps as _dumps
+
+def dumps(o):
+    return ensure_binary(_dumps(o))
diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py
index f6105e631f0f151ecf0677f9d26dc2ab912cb811..5deab0ddb9351e5ba45d2c22b29e7579c152b497 100644
--- a/src/_zkapauthorizer/_plugin.py
+++ b/src/_zkapauthorizer/_plugin.py
@@ -291,7 +291,7 @@ def _create_maintenance_service(reactor, node_config, client_node):
         get_now=get_now,
     )
     last_run_path = FilePath(
-        node_config.get_private_path(b"last-lease-maintenance-run")
+        node_config.get_private_path(u"last-lease-maintenance-run")
     )
     # Create the service to periodically run the lease maintenance operation.
     return lease_maintenance_service(
diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py
index 28670659bb8fde7d0b2cda20a2770f8a24b175b6..b47c5b3bc6c8cfc040476b978026c7d6e18258ce 100644
--- a/src/_zkapauthorizer/_storage_client.py
+++ b/src/_zkapauthorizer/_storage_client.py
@@ -20,7 +20,34 @@ This is the client part of a storage access protocol.  The server part is
 implemented in ``_storage_server.py``.
 """
 
-from __future__ import absolute_import
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from future.utils import PY2
+
+if  PY2:
+    from future.builtins import (  # noqa: F401
+        filter,
+        map,
+        zip,
+        ascii,
+        chr,
+        hex,
+        input,
+        next,
+        oct,
+        open,
+        pow,
+        round,
+        super,
+        bytes,
+        dict,
+        list,
+        object,
+        range,
+        str,
+        max,
+        min,
+    )
 
 from functools import partial, wraps
 
diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py
index 5d94598ff08f6fbd0d17db9db524a5bd2144e7ae..6e0ea94e4917ddd27d679d17fa4ef24cb6da3e5f 100644
--- a/src/_zkapauthorizer/_storage_server.py
+++ b/src/_zkapauthorizer/_storage_server.py
@@ -149,13 +149,13 @@ class _ValidationResult(object):
         """
         Cryptographically check the validity of a single pass.
 
-        :param str message: The shared message for pass validation.
+        :param bytes message: The shared message for pass validation.
         :param Pass pass_: The pass to validate.
 
         :return bool: ``False`` (invalid) if the pass includes a valid
             signature, ``True`` (valid) otherwise.
         """
-        assert isinstance(message, str), "message %r not str" % (message,)
+        assert isinstance(message, bytes), "message %r not bytes" % (message,)
         assert isinstance(pass_, Pass), "pass %r not a Pass" % (pass_,)
         try:
             preimage = TokenPreimage.decode_base64(pass_.preimage)
@@ -163,7 +163,7 @@ class _ValidationResult(object):
             unblinded_token = signing_key.rederive_unblinded_token(preimage)
             verification_key = unblinded_token.derive_verification_key_sha512()
             invalid_pass = verification_key.invalid_sha512(
-                proposed_signature, message.encode("utf-8")
+                proposed_signature, message,
             )
             return invalid_pass
         except Exception:
@@ -175,7 +175,7 @@ class _ValidationResult(object):
         """
         Check all of the given passes for validity.
 
-        :param str message: The shared message for pass validation.
+        :param bytes message: The shared message for pass validation.
         :param list[bytes] passes: The encoded passes to validate.
         :param SigningKey signing_key: The signing key to use to check the passes.
 
@@ -315,7 +315,7 @@ class ZKAPAuthorizerStorageServer(Referenceable):
         storage for immutable shares if they present valid passes.
         """
         validation = _ValidationResult.validate_passes(
-            allocate_buckets_message(storage_index),
+            allocate_buckets_message(storage_index).encode("utf-8"),
             passes,
             self._signing_key,
         )
@@ -398,7 +398,7 @@ class ZKAPAuthorizerStorageServer(Referenceable):
         duration of share storage if they present valid passes.
         """
         validation = _ValidationResult.validate_passes(
-            add_lease_message(storage_index),
+            add_lease_message(storage_index).encode("utf-8"),
             passes,
             self._signing_key,
         )
@@ -506,7 +506,7 @@ class ZKAPAuthorizerStorageServer(Referenceable):
 
         # Check passes for cryptographic validity.
         validation = _ValidationResult.validate_passes(
-            slot_testv_and_readv_and_writev_message(storage_index),
+            slot_testv_and_readv_and_writev_message(storage_index).encode("utf-8"),
             passes,
             self._signing_key,
         )
diff --git a/src/_zkapauthorizer/controller.py b/src/_zkapauthorizer/controller.py
index f9536b26b812cb423fa299c92e9195188c78a09d..e11fd5f8b6429b01bedff57ad140175e4717ab04 100644
--- a/src/_zkapauthorizer/controller.py
+++ b/src/_zkapauthorizer/controller.py
@@ -22,7 +22,7 @@ from __future__ import absolute_import, division, print_function, unicode_litera
 from future.utils import PY2
 
 if PY2:
-    from future.builtins import (
+    from future.builtins import (  # noqa: F401
         filter,
         map,
         zip,
@@ -44,7 +44,7 @@ if PY2:
         str,
         max,
         min,
-    )  # noqa: F401
+    )
 
 from base64 import b64decode, b64encode
 from datetime import timedelta
@@ -54,6 +54,7 @@ from json import dumps, loads
 from operator import delitem, setitem
 from sys import exc_info
 
+from six import ensure_text
 import attr
 import challenge_bypass_ristretto
 from treq import content
@@ -105,7 +106,7 @@ class Unpaid(Exception):
     """
 
 
-@attr.s(frozen=True)
+@attr.s
 class UnrecognizedFailureReason(Exception):
     """
     An attempt was made to redeem a voucher and the response contained an unknown reason.
@@ -121,15 +122,18 @@ class RedemptionResult(object):
     """
     Contain the results of an attempt to redeem a voucher for ZKAP material.
 
-    :ivar list[UnblindedToken] unblinded_tokens: The tokens which resulted
-        from the redemption.
+    :ivar unblinded_tokens: The tokens which resulted from the redemption.
 
-    :ivar str public_key: The public key which the server proved was
-        involved in the redemption process.
+    :ivar public_key: The public key which the server proved was involved in
+        the redemption process.
     """
 
-    unblinded_tokens = attr.ib()
-    public_key = attr.ib()
+    unblinded_tokens = attr.ib(  # type: List[UnblindedToken]
+        validator=attr.validators.instance_of(list),
+    )
+    public_key = attr.ib(  # type: str
+        validator=attr.validators.instance_of(str),
+    )
 
 
 class IRedeemer(Interface):
@@ -269,10 +273,10 @@ class ErrorRedeemer(object):
 
     @classmethod
     def make(cls, section_name, node_config, announcement, reactor):
-        details = node_config.get_config(
+        details = ensure_text(node_config.get_config(
             section=section_name,
             option="details",
-        ).decode("ascii")
+        ))
         return cls(details)
 
     def random_tokens_for_voucher(self, voucher, counter, count):
@@ -384,7 +388,7 @@ class DummyRedeemer(object):
             node_config.get_config(
                 section=section_name,
                 option="issuer-public-key",
-            ).decode("utf-8"),
+            ),
         )
 
     def random_tokens_for_voucher(self, voucher, counter, count):
@@ -486,10 +490,10 @@ class RistrettoRedeemer(object):
 
     @classmethod
     def make(cls, section_name, node_config, announcement, reactor):
-        configured_issuer = node_config.get_config(
+        configured_issuer = ensure_text(node_config.get_config(
             section=section_name,
             option="ristretto-issuer-root-url",
-        ).decode("ascii")
+        ))
         if announcement is not None:
             # Don't let us talk to a storage server that has a different idea
             # about who issues ZKAPs.  We should lift this limitation (that is, we
@@ -531,7 +535,7 @@ class RistrettoRedeemer(object):
                     "redeemVoucher": voucher.number.decode("ascii"),
                     "redeemCounter": counter,
                     "redeemTokens": list(
-                        token.encode_base64() for token in blinded_tokens
+                        ensure_text(token.encode_base64()) for token in blinded_tokens
                     ),
                 }
             ),
@@ -965,7 +969,7 @@ class PaymentController(object):
             )
             self._error[voucher] = model_Error(
                 finished=self.store.now(),
-                details=reason.getErrorMessage().decode("utf-8", "replace"),
+                details=ensure_text(reason.getErrorMessage()),
             )
         return False
 
@@ -1053,7 +1057,9 @@ def bracket(first, last, between):
     except:
         info = exc_info()
         yield last()
-        raise info[0], info[1], info[2]
+        if PY2:
+            exec("raise info[0], info[1], info[2]")
+        raise
     else:
         yield last()
         returnValue(result)
diff --git a/src/_zkapauthorizer/foolscap.py b/src/_zkapauthorizer/foolscap.py
index 20ba99fde74bfcf6f0a3f92281867887864c0d1c..c2d814eccc4ee3cf8a93aae4a140b82f8d4242a0 100644
--- a/src/_zkapauthorizer/foolscap.py
+++ b/src/_zkapauthorizer/foolscap.py
@@ -17,14 +17,15 @@ Definitions related to the Foolscap-based protocol used by ZKAPAuthorizer
 to communicate between storage clients and servers.
 """
 
-from __future__ import absolute_import
+from __future__ import absolute_import, division, print_function, unicode_literals
 
+from future.utils import PY2
 import attr
 from allmydata.interfaces import Offset, RIStorageServer, StorageIndex
 from foolscap.api import Any, Copyable, DictOf, ListOf, RemoteCopy
 from foolscap.constraint import ByteStringConstraint
 from foolscap.remoteinterface import RemoteInterface, RemoteMethodSchema
-
+from six import ensure_str
 
 @attr.s
 class ShareStat(Copyable, RemoteCopy):
@@ -37,7 +38,7 @@ class ShareStat(Copyable, RemoteCopy):
         lease on this share expires, or None if there is no lease.
     """
 
-    typeToCopy = copytype = "ShareStat"
+    typeToCopy = copytype = ensure_str("ShareStat")
 
     # To be a RemoteCopy it must be possible to instantiate this with no
     # arguments. :/ So supply defaults for these attributes.
@@ -90,7 +91,7 @@ def add_passes(schema):
     :return foolscap.remoteinterface.RemoteMethodSchema: A schema like
         ``schema`` but with one additional required argument.
     """
-    return add_arguments(schema, [(b"passes", _PassList)])
+    return add_arguments(schema, [("passes", _PassList)])
 
 
 def add_arguments(schema, kwargs):
@@ -136,7 +137,9 @@ class RIPrivacyPassAuthorizedStorageServer(RemoteInterface):
     validated is service provided.
     """
 
-    __remote_name__ = "RIPrivacyPassAuthorizedStorageServer.tahoe.privatestorage.io"
+    __remote_name__ = ensure_str(
+        "RIPrivacyPassAuthorizedStorageServer.tahoe.privatestorage.io"
+    )
 
     get_version = RIStorageServer["get_version"]
 
diff --git a/src/_zkapauthorizer/lease_maintenance.py b/src/_zkapauthorizer/lease_maintenance.py
index 49adebf5253260cbf214a0671d284cb698c061dd..4916768ba53a3fce6720de63b299c0f3232a2251 100644
--- a/src/_zkapauthorizer/lease_maintenance.py
+++ b/src/_zkapauthorizer/lease_maintenance.py
@@ -17,6 +17,35 @@ This module implements a service which periodically spends ZKAPs to
 refresh leases on all shares reachable from a root.
 """
 
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from future.utils import PY2
+
+if  PY2:
+    from future.builtins import (  # noqa: F401
+        filter,
+        map,
+        zip,
+        ascii,
+        chr,
+        hex,
+        input,
+        next,
+        oct,
+        open,
+        pow,
+        round,
+        super,
+        bytes,
+        dict,
+        list,
+        object,
+        range,
+        str,
+        max,
+        min,
+    )
+
 from datetime import datetime, timedelta
 from errno import ENOENT
 from functools import partial
@@ -26,6 +55,7 @@ try:
 except ImportError:
     pass
 
+from six import ensure_binary
 import attr
 from allmydata.interfaces import IDirectoryNode, IFilesystemNode
 from allmydata.util.hashutil import (
@@ -395,7 +425,7 @@ def lease_maintenance_service(
     """
     interval_mean = lease_maint_config.crawl_interval_mean
     interval_range = lease_maint_config.crawl_interval_range
-    halfrange = interval_range / 2
+    halfrange = interval_range // 2
 
     def sample_interval_distribution():
         return timedelta(
@@ -507,7 +537,7 @@ def write_time_to_path(path, when):
 
     :param datetime when: The datetime to write.
     """
-    path.setContent(when.isoformat())
+    path.setContent(ensure_binary(when.isoformat()))
 
 
 def read_time_from_path(path):
@@ -526,7 +556,7 @@ def read_time_from_path(path):
             return None
         raise
     else:
-        return parse_datetime(when)
+        return parse_datetime(when.decode("ascii"))
 
 
 def visit_storage_indexes_from_root(visitor, get_root_nodes):
diff --git a/src/_zkapauthorizer/model.py b/src/_zkapauthorizer/model.py
index 7caa8efa5b11357dc1cec9475f20cd3e8ba63957..fba2112dcb9146082f723930a7188b27ccc068ee 100644
--- a/src/_zkapauthorizer/model.py
+++ b/src/_zkapauthorizer/model.py
@@ -48,7 +48,7 @@ if PY2:
 
 from datetime import datetime
 from functools import wraps
-from json import dumps, loads
+from json import loads
 from sqlite3 import OperationalError
 from sqlite3 import connect as _connect
 
@@ -58,7 +58,9 @@ from past.builtins import long
 from twisted.logger import Logger
 from twisted.python.filepath import FilePath
 from zope.interface import Interface, implementer
+from six import ensure_text
 
+from ._json import dumps
 from ._base64 import urlsafe_b64decode
 from .schema import get_schema_upgrades, get_schema_version, run_schema_upgrades
 from .storage_common import (
@@ -69,16 +71,19 @@ from .storage_common import (
 from .validators import greater_than, has_length, is_base64_encoded
 
 
-def parse_datetime(s, **kw):
-    """
-    Like ``aniso8601.parse_datetime`` but accept str as well.
-    """
-    if isinstance(s, str):
-        s = s.encode("utf-8")
-    assert isinstance(s, bytes)
-    if "delimiter" in kw and isinstance(kw["delimiter"], str):
-        kw["delimiter"] = kw["delimiter"].encode("utf-8")
-    return _parse_datetime(s, **kw)
+if PY2:
+    def parse_datetime(s, **kw):
+        """
+        Like ``aniso8601.parse_datetime`` but accept str as well.
+        """
+        if isinstance(s, str):
+            s = s.encode("utf-8")
+        assert isinstance(s, bytes)
+        if "delimiter" in kw and isinstance(kw["delimiter"], str):
+            kw["delimiter"] = kw["delimiter"].encode("utf-8")
+        return _parse_datetime(s, **kw)
+else:
+    parse_datetime = _parse_datetime
 
 
 class ILeaseMaintenanceObserver(Interface):
@@ -975,7 +980,7 @@ class Redeeming(object):
     def to_json_v1(self):
         return {
             "name": "redeeming",
-            "started": self.started.isoformat(),
+            "started": ensure_text(self.started.isoformat()),
             "counter": self.counter,
         }
 
@@ -1000,7 +1005,7 @@ class Redeemed(object):
     def to_json_v1(self):
         return {
             "name": "redeemed",
-            "finished": self.finished.isoformat(),
+            "finished": ensure_text(self.finished.isoformat()),
             "token-count": self.token_count,
         }
 
@@ -1015,7 +1020,7 @@ class DoubleSpend(object):
     def to_json_v1(self):
         return {
             "name": "double-spend",
-            "finished": self.finished.isoformat(),
+            "finished": ensure_text(self.finished.isoformat()),
         }
 
 
@@ -1035,7 +1040,7 @@ class Unpaid(object):
     def to_json_v1(self):
         return {
             "name": "unpaid",
-            "finished": self.finished.isoformat(),
+            "finished": ensure_text(self.finished.isoformat()),
         }
 
 
@@ -1056,7 +1061,7 @@ class Error(object):
     def to_json_v1(self):
         return {
             "name": "error",
-            "finished": self.finished.isoformat(),
+            "finished": ensure_text(self.finished.isoformat()),
             "details": self.details,
         }
 
@@ -1204,7 +1209,7 @@ class Voucher(object):
         return {
             "number": self.number.decode("ascii"),
             "expected-tokens": self.expected_tokens,
-            "created": None if self.created is None else self.created.isoformat(),
+            "created": None if self.created is None else ensure_text(self.created.isoformat()),
             "state": state,
             "version": 1,
         }
diff --git a/src/_zkapauthorizer/resource.py b/src/_zkapauthorizer/resource.py
index 9dbb51e2fe57385510b5cfe19d74605a9a5c2b7f..82d787e545129aff07609e02894a72d1022df621 100644
--- a/src/_zkapauthorizer/resource.py
+++ b/src/_zkapauthorizer/resource.py
@@ -51,9 +51,10 @@ if PY2:
     )
 
 from itertools import islice
-from json import dumps, load, loads
-from sys import maxint
+from json import load, loads
+from sys import maxsize
 
+from six import ensure_str, ensure_binary
 from past.builtins import long
 from twisted.logger import Logger
 from twisted.web.http import BAD_REQUEST
@@ -61,6 +62,7 @@ from twisted.web.resource import ErrorPage, IResource, NoResource, Resource
 from twisted.web.server import NOT_DONE_YET
 from zope.interface import Attribute
 
+from ._json import dumps
 from . import __version__ as _zkapauthorizer_version
 from ._base64 import urlsafe_b64decode
 from .config import get_configured_lease_duration
@@ -170,7 +172,7 @@ def from_configuration(
     )
 
     root = create_private_tree(
-        lambda: node_config.get_private_config(b"api_auth_token"),
+        lambda: ensure_binary(node_config.get_private_config(ensure_str("api_auth_token"))),
         authorizationless_resource_tree(
             store,
             controller,
@@ -373,7 +375,7 @@ class _UnblindedTokenCollection(Resource):
 
         limit = request.args.get(b"limit", [None])[0]
         if limit is not None:
-            limit = min(maxint, int(limit))
+            limit = min(maxsize, int(limit))
 
         position = request.args.get(b"position", [b""])[0].decode("utf-8")
 
@@ -432,7 +434,7 @@ class _VoucherCollection(Resource):
             payload = loads(request.content.read())
         except Exception:
             return bad_request("json request body required").render(request)
-        if payload.keys() != ["voucher"]:
+        if set(payload) != {"voucher"}:
             return bad_request(
                 "request object must have exactly one key: 'voucher'"
             ).render(request)
diff --git a/src/_zkapauthorizer/spending.py b/src/_zkapauthorizer/spending.py
index 19b96ecb1f597e0ddbe26e1840853c6af7b0ef64..49983f4eafbc0ddb6c1eebcd650142fb0608c358 100644
--- a/src/_zkapauthorizer/spending.py
+++ b/src/_zkapauthorizer/spending.py
@@ -16,6 +16,35 @@
 A module for logic controlling the manner in which ZKAPs are spent.
 """
 
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from future.utils import PY2
+
+if  PY2:
+    from future.builtins import (  # noqa: F401
+        filter,
+        map,
+        zip,
+        ascii,
+        chr,
+        hex,
+        input,
+        next,
+        oct,
+        open,
+        pow,
+        round,
+        super,
+        bytes,
+        dict,
+        list,
+        object,
+        range,
+        str,
+        max,
+        min,
+    )
+
 import attr
 from zope.interface import Attribute, Interface, implementer
 
@@ -101,7 +130,7 @@ class PassGroup(object):
     """
     Track the state of a group of passes intended as payment for an operation.
 
-    :ivar unicode _message: The request binding message for this group of
+    :ivar _message: The request binding message for this group of
         passes.
 
     :ivar IPassFactory _factory: The factory which created this pass group.
@@ -109,19 +138,22 @@ class PassGroup(object):
     :ivar list[Pass] passes: The passes of which this group consists.
     """
 
-    _message = attr.ib()
-    _factory = attr.ib()
-    _tokens = attr.ib()
+    _message = attr.ib(validator=attr.validators.instance_of(bytes))  # type: bytes
+    _factory = attr.ib(validator=attr.validators.provides(IPassFactory))  # type: IPassFactory
+    _tokens = attr.ib(validator=attr.validators.instance_of(list))  # type: List[(UnblinidedToken, Pass)]
 
     @property
     def passes(self):
+        # type: () -> List[Pass]
         return list(pass_ for (unblinded_token, pass_) in self._tokens)
 
     @property
     def unblinded_tokens(self):
+        # type: () -> List[UnblindedToken]
         return list(unblinded_token for (unblinded_token, pass_) in self._tokens)
 
     def split(self, select_indices):
+        # type: (List[int]) -> (PassGroup, PassGroup)
         selected = []
         unselected = []
         for idx, t in enumerate(self._tokens):
@@ -135,18 +167,22 @@ class PassGroup(object):
         )
 
     def expand(self, by_amount):
+        # type: (int) -> PassGroup
         return attr.evolve(
             self,
             tokens=self._tokens + self._factory.get(self._message, by_amount)._tokens,
         )
 
     def mark_spent(self):
+        # type: () -> None
         self._factory._mark_spent(self.unblinded_tokens)
 
     def mark_invalid(self, reason):
+        # type: () -> None
         self._factory._mark_invalid(reason, self.unblinded_tokens)
 
     def reset(self):
+        # tye: () -> None
         self._factory._reset(self.unblinded_tokens)
 
 
@@ -158,12 +194,12 @@ class SpendingController(object):
     attempts when necessary.
     """
 
-    get_unblinded_tokens = attr.ib()
-    discard_unblinded_tokens = attr.ib()
-    invalidate_unblinded_tokens = attr.ib()
-    reset_unblinded_tokens = attr.ib()
+    get_unblinded_tokens = attr.ib()  # type: (int) -> [UnblindedToken]
+    discard_unblinded_tokens = attr.ib()  # type: ([UnblindedTokens]) -> None
+    invalidate_unblinded_tokens = attr.ib()  # type: ([UnblindedTokens]) -> None
+    reset_unblinded_tokens = attr.ib()  # type: ([UnblindedTokens]) -> None
 
-    tokens_to_passes = attr.ib()
+    tokens_to_passes = attr.ib() # type: (bytes, [UnblindedTokens]) -> [Pass]
 
     @classmethod
     def for_store(cls, tokens_to_passes, store):
@@ -179,10 +215,10 @@ class SpendingController(object):
         unblinded_tokens = self.get_unblinded_tokens(num_passes)
         passes = self.tokens_to_passes(message, unblinded_tokens)
         GET_PASSES.log(
-            message=message,
+            message=message.decode("utf-8"),
             count=num_passes,
         )
-        return PassGroup(message, self, zip(unblinded_tokens, passes))
+        return PassGroup(message, self, list(zip(unblinded_tokens, passes)))
 
     def _mark_spent(self, unblinded_tokens):
         SPENT_PASSES.log(
diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py
index b847073a5cc7a1c857ff2d9874b087ceae2397c0..b9c4b2cf6880d722066f46447264af90566c6dac 100644
--- a/src/_zkapauthorizer/storage_common.py
+++ b/src/_zkapauthorizer/storage_common.py
@@ -55,7 +55,7 @@ from .eliot import MUTABLE_PASSES_REQUIRED
 from .validators import greater_than
 
 
-@attr.s(frozen=True, str=True)
+@attr.s(str=True)
 class MorePassesRequired(Exception):
     """
     Storage operations fail with ``MorePassesRequired`` when they are not
@@ -80,7 +80,7 @@ def _message_maker(label):
     def make_message(storage_index):
         return "{label} {storage_index}".format(
             label=label,
-            storage_index=b64encode(storage_index),
+            storage_index=b64encode(storage_index).decode("utf-8"),
         )
 
     return make_message
diff --git a/src/_zkapauthorizer/tests/fixtures.py b/src/_zkapauthorizer/tests/fixtures.py
index e017eadd6b42f10f6f4255caf5c110d64cfca7de..2bc62c8e6c35f35beefac24b31514499e975c83e 100644
--- a/src/_zkapauthorizer/tests/fixtures.py
+++ b/src/_zkapauthorizer/tests/fixtures.py
@@ -16,7 +16,34 @@
 Common fixtures to let the test suite focus on application logic.
 """
 
-from __future__ import absolute_import
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from future.utils import PY2
+
+if PY2:
+    from future.builtins import (  # noqa: F401
+        filter,
+        map,
+        zip,
+        ascii,
+        chr,
+        hex,
+        input,
+        next,
+        oct,
+        open,
+        pow,
+        round,
+        super,
+        bytes,
+        dict,
+        list,
+        object,
+        range,
+        str,
+        max,
+        min,
+    )
 
 from base64 import b64encode
 
@@ -49,9 +76,9 @@ class AnonymousStorageServer(Fixture):
     clock = attr.ib()
 
     def _setUp(self):
-        self.tempdir = FilePath(self.useFixture(TempDir()).join(b"storage"))
+        self.tempdir = FilePath(self.useFixture(TempDir()).join(u"storage"))
         self.storage_server = StorageServer(
-            self.tempdir.asBytesMode().path,
+            self.tempdir.asTextMode().path,
             b"x" * 20,
             clock=self.clock,
         )
@@ -75,7 +102,7 @@ class TemporaryVoucherStore(Fixture):
 
     def _setUp(self):
         self.tempdir = self.useFixture(TempDir())
-        self.config = self.get_config(self.tempdir.join(b"node"), b"tub.port")
+        self.config = self.get_config(self.tempdir.join(u"node"), u"tub.port")
         self.store = VoucherStore.from_node_config(
             self.config,
             self.get_now,
diff --git a/src/_zkapauthorizer/tests/foolscap.py b/src/_zkapauthorizer/tests/foolscap.py
index 3a984bea163fd4c567812556f8229508c0cb8a2d..e2d1c43f32324f9895b83324634c2f426b6463f7 100644
--- a/src/_zkapauthorizer/tests/foolscap.py
+++ b/src/_zkapauthorizer/tests/foolscap.py
@@ -121,7 +121,7 @@ class LocalRemote(object):
             schema = self._referenceable.getInterface()[methname]
             if self.check_args:
                 schema.checkAllArgs(args, kwargs, inbound=True)
-            _check_copyables(list(args) + kwargs.values())
+            _check_copyables(list(args) + list(kwargs.values()))
             result = self._referenceable.doRemoteCall(
                 methname,
                 args,
diff --git a/src/_zkapauthorizer/tests/privacypass.py b/src/_zkapauthorizer/tests/privacypass.py
index a2c29cc4b9daee16bfb86f1f311f1b1c4b45e4f6..0f4287730b66b021c41dc549b535b6e06bdfc5f0 100644
--- a/src/_zkapauthorizer/tests/privacypass.py
+++ b/src/_zkapauthorizer/tests/privacypass.py
@@ -30,7 +30,7 @@ def make_passes(signing_key, for_message, random_tokens):
     :param challenge_bypass_ristretto.SigningKey signing_key: The key to use
         to sign the passes.
 
-    :param unicode for_message: The request-binding message with which to
+    :param bytes for_message: The request-binding message with which to
         associate the passes.
 
     :param list[challenge_bypass_ristretto.RandomToken] random_tokens: The
@@ -62,7 +62,7 @@ def make_passes(signing_key, for_message, random_tokens):
         for unblinded_signature in unblinded_signatures
     )
     message_signatures = list(
-        verification_key.sign_sha512(for_message.encode("utf-8"))
+        verification_key.sign_sha512(for_message)
         for verification_key in verification_keys
     )
     passes = list(
diff --git a/src/_zkapauthorizer/tests/storage_common.py b/src/_zkapauthorizer/tests/storage_common.py
index db95a71c78308c472b7ab60f96cb09e50a4e9ac4..61c061c55d71dd240aa37b8095d6597f8973d3b5 100644
--- a/src/_zkapauthorizer/tests/storage_common.py
+++ b/src/_zkapauthorizer/tests/storage_common.py
@@ -154,7 +154,7 @@ def integer_passes(limit):
 
 def get_passes(message, count, signing_key):
     """
-    :param unicode message: Request-binding message for PrivacyPass.
+    :param bytes message: Request-binding message for PrivacyPass.
 
     :param int count: The number of passes to get.
 
@@ -163,6 +163,7 @@ def get_passes(message, count, signing_key):
     :return list[Pass]: ``count`` new random passes signed with the given key
         and bound to the given message.
     """
+    assert isinstance(message, bytes)
     return make_passes(
         signing_key,
         message,
@@ -200,33 +201,33 @@ class _PassFactory(object):
     """
     A stateful pass issuer.
 
-    :ivar (unicode -> int -> [bytes]) _get_passes: A function for getting
-        passes.
+    :ivar _get_passes: A function for getting passes.
 
-    :ivar set[int] in_use: All of the passes given out without a confirmed
+    :ivar in_use: All of the passes given out without a confirmed
         terminal state.
 
-    :ivar dict[int, unicode] invalid: All of the passes given out and returned
-        using ``IPassGroup.invalid`` mapped to the reason given.
+    :ivar invalid: All of the passes given out and returned using
+        ``IPassGroup.invalid`` mapped to the reason given.
 
-    :ivar set[int] spent: All of the passes given out and returned via
+    :ivar spent: All of the passes given out and returned via
         ``IPassGroup.mark_spent``.
 
-    :ivar set[int] issued: All of the passes ever given out.
+    :ivar issued: All of the passes ever given out.
 
-    :ivar list[int] returned: A list of passes which were given out but then
-        returned via ``IPassGroup.reset``.
+    :ivar returned: A list of passes which were given out but then returned
+        via ``IPassGroup.reset``.
     """
 
-    _get_passes = attr.ib()
+    _get_passes = attr.ib()  # type: (bytes, int) -> List[bytes]
 
-    returned = attr.ib(default=attr.Factory(list), init=False)
-    in_use = attr.ib(default=attr.Factory(set), init=False)
-    invalid = attr.ib(default=attr.Factory(dict), init=False)
-    spent = attr.ib(default=attr.Factory(set), init=False)
-    issued = attr.ib(default=attr.Factory(set), init=False)
+    returned = attr.ib(default=attr.Factory(list), init=False)  # type: List[int]
+    in_use = attr.ib(default=attr.Factory(set), init=False)  # type: Set[int]
+    invalid = attr.ib(default=attr.Factory(dict), init=False)  # type: Dict[int, unicode]
+    spent = attr.ib(default=attr.Factory(set), init=False)  # type: Set[int]
+    issued = attr.ib(default=attr.Factory(set), init=False)  # type: Set[int]
 
     def get(self, message, num_passes):
+        # type: (bytes, int) -> PassGroup
         passes = []
         if self.returned:
             passes.extend(self.returned[:num_passes])
@@ -235,7 +236,7 @@ class _PassFactory(object):
         passes.extend(self._get_passes(message, num_passes))
         self.issued.update(passes)
         self.in_use.update(passes)
-        return PassGroup(message, self, zip(passes, passes))
+        return PassGroup(message, self, list(zip(passes, passes)))
 
     def _clear(self):
         """
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index 7f29860e6046ce6e4bdab528e844210bc1193329..54306ea102d22e38a8a6029276d96aec2d4a8840 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -47,7 +47,6 @@ if PY2:
 
 from base64 import b64encode, urlsafe_b64encode
 from datetime import datetime, timedelta
-from urllib import quote
 
 import attr
 from allmydata.client import config_from_string
@@ -70,6 +69,8 @@ from hypothesis.strategies import (
     text,
     tuples,
 )
+from six.moves.urllib.parse import quote
+from six import ensure_binary
 from twisted.internet.defer import succeed
 from twisted.internet.task import Clock
 from twisted.web.test.requesthelper import DummyRequest
@@ -554,7 +555,7 @@ def tahoe_configs(
 
     def path_setter(config):
         def set_paths(basedir, portnumfile):
-            config._basedir = basedir.decode("ascii")
+            config._basedir = basedir
             config.portnum_fname = portnumfile
             return config
 
@@ -757,7 +758,13 @@ def request_paths():
 
     :see: ``requests``
     """
-    return lists(text().map(lambda x: quote(x.encode("utf-8"), safe=b"")))
+    def quote_segment(seg):
+        if PY2:
+            return quote(seg.encode("utf-8"), safe=b"")
+        else:
+            return quote(seg, safe="").encode("utf-8")
+
+    return lists(text().map(quote_segment))
 
 
 def requests(paths=request_paths()):
diff --git a/src/_zkapauthorizer/tests/test_client_resource.py b/src/_zkapauthorizer/tests/test_client_resource.py
index f559b3e135bb34db7289ce8186bd88dac9fa84ff..471a41a7d5f28feed3a5238bb5c4990fbe43e386 100644
--- a/src/_zkapauthorizer/tests/test_client_resource.py
+++ b/src/_zkapauthorizer/tests/test_client_resource.py
@@ -48,8 +48,8 @@ if PY2:
 
 from datetime import datetime
 from io import BytesIO
-from json import dumps
-from urllib import quote
+from six.moves.urllib.parse import quote
+from six import ensure_binary, ensure_text
 
 import attr
 from allmydata.client import config_from_string
@@ -98,6 +98,7 @@ from twisted.web.http import BAD_REQUEST, NOT_FOUND, NOT_IMPLEMENTED, OK, UNAUTH
 from twisted.web.http_headers import Headers
 from twisted.web.resource import IResource, getChildForRequest
 
+from .. _json import dumps
 from .. import __version__ as zkapauthorizer_version
 from .._base64 import urlsafe_b64decode
 from ..configutil import config_string_from_sections
@@ -164,17 +165,17 @@ def is_not_json(bytestring):
 
 def not_vouchers():
     """
-    Builds text strings which are not legal vouchers.
+    Builds byte strings which are not legal vouchers.
     """
     return one_of(
         text().filter(
             lambda t: (not is_urlsafe_base64(t)),
-        ),
+        ).map(lambda t: t.encode("utf-8")),
         vouchers().map(
             # Turn a valid voucher into a voucher that is invalid only by
             # containing a character from the base64 alphabet in place of one
             # from the urlsafe-base64 alphabet.
-            lambda voucher: "/"
+            lambda voucher: b"/"
             + voucher[1:],
         ),
     )
@@ -201,7 +202,7 @@ def invalid_bodies():
         # The wrong key but the right kind of value.
         fixed_dictionaries(
             {
-                "some-key": vouchers(),
+                "some-key": vouchers().map(ensure_text),
             }
         ).map(dumps),
         # The right key but the wrong kind of value.
@@ -209,7 +210,7 @@ def invalid_bodies():
             {
                 "voucher": one_of(
                     integers(),
-                    not_vouchers(),
+                    not_vouchers().map(ensure_text),
                 ),
             }
         ).map(dumps),
@@ -270,7 +271,7 @@ def authorized_request(api_auth_token, agent, method, uri, headers=None, data=No
         headers = Headers(headers)
     headers.setRawHeaders(
         "authorization",
-        [b"tahoe-lafs {}".format(api_auth_token)],
+        [b"tahoe-lafs " + api_auth_token],
     )
     return agent.request(
         method,
@@ -294,8 +295,8 @@ def get_config_with_api_token(tempdir, get_config, api_auth_token):
     :param bytes api_auth_token: The HTTP API authorization token to write to
         the node directory.
     """
-    basedir = tempdir.join(b"tahoe")
-    config = get_config(basedir, b"tub.port")
+    basedir = tempdir.join(u"tahoe")
+    config = get_config(basedir, u"tub.port")
     add_api_token_to_config(
         basedir,
         config,
@@ -309,9 +310,9 @@ def add_api_token_to_config(basedir, config, api_auth_token):
     Create a private directory beneath the given base directory, point the
     given config at it, and write the given API auth token to it.
     """
-    FilePath(basedir).child(b"private").makedirs()
+    FilePath(basedir).child(u"private").makedirs()
     config._basedir = basedir
-    config.write_private_config(b"api_auth_token", api_auth_token)
+    config.write_private_config(u"api_auth_token", api_auth_token)
 
 
 class FromConfigurationTests(TestCase):
@@ -326,7 +327,7 @@ class FromConfigurationTests(TestCase):
         the public keys found in the configuration.
         """
         tempdir = self.useFixture(TempDir())
-        config = get_config(tempdir.join(b"tahoe"), b"tub.port")
+        config = get_config(tempdir.join("tahoe"), "tub.port")
         allowed_public_keys = get_configured_allowed_public_keys(config)
 
         # root_from_config is just an easier way to call from_configuration
@@ -366,7 +367,7 @@ class GetTokenCountTests(TestCase):
             ]
         )
         node_config = config_from_string(
-            self.useFixture(TempDir()).join(b"tahoe"),
+            self.useFixture(TempDir()).join("tahoe"),
             "tub.port",
             config_text.encode("utf-8"),
         )
@@ -391,7 +392,7 @@ class ResourceTests(TestCase):
         receives a 401 response.
         """
         tempdir = self.useFixture(TempDir())
-        config = get_config(tempdir.join(b"tahoe"), b"tub.port")
+        config = get_config(tempdir.join("tahoe"), "tub.port")
         root = root_from_config(config, datetime.now)
         agent = RequestTraversalAgent(root)
         requesting = agent.request(
@@ -429,7 +430,7 @@ class ResourceTests(TestCase):
         ``from_configuration``.
         """
         tempdir = self.useFixture(TempDir())
-        config = get_config(tempdir.join(b"tahoe"), b"tub.port")
+        config = get_config(tempdir.join("tahoe"), "tub.port")
         root = root_from_config(config, datetime.now)
         self.assertThat(
             getChildForRequest(root, request),
@@ -545,7 +546,7 @@ class UnblindedTokenTests(TestCase):
 
         self.assertThat(
             stored_tokens,
-            Equals(list(token.unblinded_token for token in unblinded_tokens)),
+            Equals(list(token.unblinded_token.decode("ascii") for token in unblinded_tokens)),
         )
 
     @given(
@@ -632,7 +633,7 @@ class UnblindedTokenTests(TestCase):
             api_auth_token,
             agent,
             b"GET",
-            b"http://127.0.0.1/unblinded-token?limit={}".format(limit),
+            u"http://127.0.0.1/unblinded-token?limit={}".format(limit).encode("utf-8"),
         )
         self.addDetail(
             "requesting result",
@@ -685,9 +686,9 @@ class UnblindedTokenTests(TestCase):
             api_auth_token,
             agent,
             b"GET",
-            b"http://127.0.0.1/unblinded-token?position={}".format(
+            u"http://127.0.0.1/unblinded-token?position={}".format(
                 quote(position.encode("utf-8"), safe=b""),
-            ),
+            ).encode("utf-8"),
         )
         self.addDetail(
             "requesting result",
@@ -1033,11 +1034,11 @@ class VoucherTests(TestCase):
         )
         root = root_from_config(config, datetime.now)
         agent = RequestTraversalAgent(root)
-        url = "http://127.0.0.1/voucher/{}".format(
+        url = u"http://127.0.0.1/voucher/{}".format(
             quote(
-                not_voucher.encode("utf-8"),
+                not_voucher,
                 safe=b"",
-            ).decode("utf-8"),
+            ),
         ).encode("ascii")
         requesting = authorized_request(
             api_auth_token,
@@ -1070,7 +1071,7 @@ class VoucherTests(TestCase):
             api_auth_token,
             agent,
             b"GET",
-            "http://127.0.0.1/voucher/{}".format(voucher).encode("ascii"),
+            b"http://127.0.0.1/voucher/" + voucher,
         )
         self.assertThat(
             requesting,
@@ -1251,7 +1252,7 @@ class VoucherTests(TestCase):
             to be returned by the ``GET``.
         """
         add_api_token_to_config(
-            self.useFixture(TempDir()).join(b"tahoe"),
+            self.useFixture(TempDir()).join("tahoe"),
             config,
             api_auth_token,
         )
@@ -1275,11 +1276,11 @@ class VoucherTests(TestCase):
             api_auth_token,
             agent,
             b"GET",
-            "http://127.0.0.1/voucher/{}".format(
+            u"http://127.0.0.1/voucher/{}".format(
                 quote(
-                    voucher.encode("utf-8"),
-                    safe=b"",
-                ).decode("utf-8"),
+                    voucher,
+                    safe=u"",
+                ),
             ).encode("ascii"),
         )
         self.assertThat(
@@ -1379,7 +1380,7 @@ class VoucherTests(TestCase):
             # times between setUp and tearDown.  Avoid re-using the same
             # temporary directory for every Hypothesis iteration because this
             # test leaves state behind that invalidates future iterations.
-            self.useFixture(TempDir()).join(b"tahoe"),
+            self.useFixture(TempDir()).join("tahoe"),
             config,
             api_auth_token,
         )
@@ -1431,9 +1432,10 @@ def mime_types(blacklist=None):
     """
     Build MIME types as b"major/minor" byte strings.
 
-    :param set|None blacklist: If not ``None``, MIME types to exclude from the
-        result.
+    :param blacklist: If not ``None``, MIME types to
+        exclude from the result.
     """
+    # type: Optional[Set[unicode]] -> Strategy[unicode]
     if blacklist is None:
         blacklist = set()
     return (
@@ -1442,7 +1444,7 @@ def mime_types(blacklist=None):
             text(),
         )
         .map(
-            b"/".join,
+            u"/".join,
         )
         .filter(
             lambda content_type: content_type not in blacklist,
@@ -1482,7 +1484,7 @@ def bad_calculate_price_requests():
     bad_headers = fixed_dictionaries(
         {
             b"content-type": mime_types(blacklist={b"application/json"},).map(
-                lambda content_type: [content_type],
+                lambda content_type: [content_type.encode("utf-8")],
             ),
         }
     )
@@ -1645,7 +1647,7 @@ class CalculatePriceTests(TestCase):
         (encoding_params, min_time_remaining), config = encoding_params_and_config
         shares_needed, shares_happy, shares_total = encoding_params
         add_api_token_to_config(
-            self.useFixture(TempDir()).join(b"tahoe"),
+            self.useFixture(TempDir()).join("tahoe"),
             config,
             api_auth_token,
         )
@@ -1699,7 +1701,7 @@ def json_content(response):
 
 
 def ok_response(headers=None):
-    return match_response(OK, headers)
+    return match_response(OK, headers, phrase=Equals(b"OK"))
 
 
 def not_found_response(headers=None):
@@ -1710,12 +1712,13 @@ def bad_request_response(headers=None):
     return match_response(BAD_REQUEST, headers)
 
 
-def match_response(code, headers):
+def match_response(code, headers, phrase=Always()):
     if headers is None:
         headers = Always()
     return _MatchResponse(
         code=Equals(code),
         headers=headers,
+        phrase=phrase,
     )
 
 
@@ -1723,6 +1726,7 @@ def match_response(code, headers):
 class _MatchResponse(object):
     code = attr.ib()
     headers = attr.ib()
+    phrase = attr.ib()
     _details = attr.ib(default=attr.Factory(dict))
 
     def match(self, response):
@@ -1735,6 +1739,7 @@ class _MatchResponse(object):
         return MatchesStructure(
             code=self.code,
             headers=self.headers,
+            phrase=self.phrase,
         ).match(response)
 
     def get_details(self):
diff --git a/src/_zkapauthorizer/tests/test_controller.py b/src/_zkapauthorizer/tests/test_controller.py
index 4742a1462882b9f49639931d9b531be84a08ebf6..4b4f399b1ff7965a43b700d066bce888a719cd0d 100644
--- a/src/_zkapauthorizer/tests/test_controller.py
+++ b/src/_zkapauthorizer/tests/test_controller.py
@@ -20,9 +20,10 @@ from __future__ import absolute_import, division
 
 from datetime import datetime, timedelta
 from functools import partial
-from json import dumps, loads
+from json import loads
 
 import attr
+from six import ensure_text
 from challenge_bypass_ristretto import (
     BatchDLEQProof,
     BlindedToken,
@@ -49,7 +50,7 @@ from testtools.matchers import (
 )
 from testtools.twistedsupport import failed, has_no_result, succeeded
 from treq.testing import StubTreq
-from twisted.internet.defer import fail
+from twisted.internet.defer import fail, succeed
 from twisted.internet.task import Clock
 from twisted.python.url import URL
 from twisted.web.http import BAD_REQUEST, INTERNAL_SERVER_ERROR, UNSUPPORTED_MEDIA_TYPE
@@ -58,6 +59,7 @@ from twisted.web.iweb import IAgent
 from twisted.web.resource import ErrorPage, Resource
 from zope.interface import implementer
 
+from .._json import dumps
 from ..controller import (
     AlreadySpent,
     DoubleSpendRedeemer,
@@ -794,7 +796,7 @@ class RistrettoRedeemerTests(TestCase):
                         HasLength(num_tokens),
                     ),
                     public_key=Equals(
-                        PublicKey.from_signing_key(signing_key).encode_base64(),
+                        ensure_text(PublicKey.from_signing_key(signing_key).encode_base64()),
                     ),
                 ),
             ),
@@ -1156,9 +1158,9 @@ class RistrettoRedemption(Resource):
         return dumps(
             {
                 u"success": True,
-                u"public-key": self.public_key.encode_base64(),
-                u"signatures": marshaled_signed_tokens,
-                u"proof": marshaled_proof,
+                u"public-key": ensure_text(self.public_key.encode_base64()),
+                u"signatures": list(ensure_text(t) for t in marshaled_signed_tokens),
+                u"proof": ensure_text(marshaled_proof),
             }
         )
 
@@ -1250,7 +1252,7 @@ def check_redemption_request(request):
     Verify that the given request conforms to the redemption server's public
     interface.
     """
-    if request.requestHeaders.getRawHeaders(b"content-type") != ["application/json"]:
+    if request.requestHeaders.getRawHeaders(b"content-type") != [b"application/json"]:
         return bad_content_type(request)
 
     p = request.content.tell()
@@ -1293,10 +1295,15 @@ def bad_content_type(request):
     ).render(request)
 
 
-class BracketTests(TestCase):
+class _BracketTestMixin:
     """
     Tests for ``bracket``.
     """
+    def wrap_success(self, result):
+        raise NotImplemented()
+
+    def wrap_failure(self, result):
+        raise NotImplemented()
 
     def test_success(self):
         """
@@ -1309,7 +1316,7 @@ class BracketTests(TestCase):
 
         def between():
             actions.append("between")
-            return result
+            return self.wrap_success(result)
 
         last = partial(actions.append, "last")
         self.assertThat(
@@ -1337,7 +1344,7 @@ class BracketTests(TestCase):
 
         def between():
             actions.append("between")
-            raise SomeException()
+            return self.wrap_failure(SomeException())
 
         last = partial(actions.append, "last")
         self.assertThat(
@@ -1349,3 +1356,121 @@ class BracketTests(TestCase):
                 ),
             ),
         )
+        self.assertThat(
+            actions,
+            Equals(["first", "between", "last"]),
+        )
+
+    def test_success_with_failing_last(self):
+        """
+        If the ``between`` action succeeds and the ``last`` action fails then
+        ``bracket`` fails the same way as the ``last`` action.
+        """
+
+        class SomeException(Exception):
+            pass
+
+        actions = []
+        first = partial(actions.append, "first")
+        def between():
+            actions.append("between")
+            return self.wrap_success(None)
+
+        def last():
+            actions.append("last")
+            return self.wrap_failure(SomeException())
+
+        self.assertThat(
+            bracket(first, last, between),
+            failed(
+                AfterPreprocessing(
+                    lambda failure: failure.value,
+                    IsInstance(SomeException),
+                ),
+            ),
+        )
+        self.assertThat(
+            actions,
+            Equals(["first", "between", "last"]),
+        )
+
+    def test_failure_with_failing_last(self):
+        """
+        If both the ``between`` and ``last`` actions fail then ``bracket`` fails
+        the same way as the ``last`` action.
+        """
+
+        class SomeException(Exception):
+            pass
+
+        class AnotherException(Exception):
+            pass
+
+        actions = []
+        first = partial(actions.append, "first")
+
+        def between():
+            actions.append("between")
+            return self.wrap_failure(SomeException())
+
+        def last():
+            actions.append("last")
+            return self.wrap_failure(AnotherException())
+
+        self.assertThat(
+            bracket(first, last, between),
+            failed(
+                AfterPreprocessing(
+                    lambda failure: failure.value,
+                    IsInstance(AnotherException),
+                ),
+            ),
+        )
+        self.assertThat(
+            actions,
+            Equals(["first", "between", "last"]),
+        )
+
+    def test_first_failure(self):
+        """
+        If the ``first`` action fails then ``bracket`` fails the same way and
+        runs neither the ``between`` nor ``last`` actions.
+        """
+
+        class SomeException(Exception):
+            pass
+
+        actions = []
+
+        def first():
+            actions.append("first")
+            return self.wrap_failure(SomeException())
+
+        between = partial(actions.append, "between")
+        last = partial(actions.append, "last")
+
+        self.assertThat(
+            bracket(first, last, between),
+            failed(
+                AfterPreprocessing(
+                    lambda failure: failure.value,
+                    IsInstance(SomeException),
+                ),
+            ),
+        )
+        self.assertThat(
+            actions,
+            Equals(["first"]),
+        )
+
+class BracketTests(_BracketTestMixin, TestCase):
+    def wrap_success(self, result):
+        return result
+    def wrap_failure(self, exception):
+        raise exception
+
+class SynchronousDeferredBracketTests(_BracketTestMixin, TestCase):
+    def wrap_success(self, result):
+        return succeed(result)
+    def wrap_failure(self, exception):
+        return fail(exception)
diff --git a/src/_zkapauthorizer/tests/test_foolscap.py b/src/_zkapauthorizer/tests/test_foolscap.py
index 3a313b879aa720caf31b20ce97b191d9289e1424..1dee261cd41fc4867b0f7e963a7418ce8d737d4e 100644
--- a/src/_zkapauthorizer/tests/test_foolscap.py
+++ b/src/_zkapauthorizer/tests/test_foolscap.py
@@ -16,8 +16,36 @@
 Tests for Foolscap-related test helpers.
 """
 
-from __future__ import absolute_import
-
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from future.utils import PY2
+
+# if PY2:
+#     from future.builtins import (  # noqa: F401
+#         filter,
+#         map,
+#         zip,
+#         ascii,
+#         chr,
+#         hex,
+#         input,
+#         next,
+#         oct,
+#         open,
+#         pow,
+#         round,
+#         super,
+#         bytes,
+#         dict,
+#         list,
+#         object,
+#         range,
+#         str,
+#         max,
+#         min,
+#     )
+
+from six import ensure_str
 from fixtures import Fixture
 from foolscap.api import Any, RemoteInterface, Violation
 from foolscap.furl import decode_furl
@@ -55,7 +83,7 @@ class IHasSchema(RemoteInterface):
 def remote_reference():
     tub = Tub()
     tub.setLocation("127.0.0.1:12345")
-    url = tub.buildURL(b"efgh")
+    url = tub.buildURL(ensure_str("efgh"))
 
     # Ugh ugh ugh.  Skip over the extra correctness checking in
     # RemoteReferenceTracker.__init__ that requires having a broker by passing
@@ -85,12 +113,9 @@ class LocalRemoteTests(TestCase):
         """
         self.assertThat(
             ref.tracker.getURL(),
-            MatchesAll(
-                IsInstance(bytes),
-                AfterPreprocessing(
-                    decode_furl,
-                    Always(),
-                ),
+            AfterPreprocessing(
+                decode_furl,
+                Always(),
             ),
         )
 
diff --git a/src/_zkapauthorizer/tests/test_lease_maintenance.py b/src/_zkapauthorizer/tests/test_lease_maintenance.py
index 02e2230c2ae2b6ad9b4d97e7c9149aeceb928722..15d9ca9765dae9dc21af780a0cee9ff4fa3503a9 100644
--- a/src/_zkapauthorizer/tests/test_lease_maintenance.py
+++ b/src/_zkapauthorizer/tests/test_lease_maintenance.py
@@ -16,7 +16,34 @@
 Tests for ``_zkapauthorizer.lease_maintenance``.
 """
 
-from __future__ import absolute_import, unicode_literals
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from future.utils import PY2
+
+if  PY2:
+    from future.builtins import (  # noqa: F401
+        filter,
+        map,
+        zip,
+        ascii,
+        chr,
+        hex,
+        input,
+        next,
+        oct,
+        open,
+        pow,
+        round,
+        super,
+        bytes,
+        dict,
+        list,
+        object,
+        range,
+        str,
+        max,
+        min,
+    )
 
 from datetime import datetime, timedelta
 
@@ -37,6 +64,7 @@ from hypothesis.strategies import (
     randoms,
     sets,
 )
+from six import ensure_binary
 from testtools import TestCase
 from testtools.matchers import (
     AfterPreprocessing,
@@ -284,8 +312,8 @@ class LeaseMaintenanceServiceTests(TestCase):
         [maintenance_call] = clock.getDelayedCalls()
 
         datetime_now = datetime.utcfromtimestamp(clock.seconds())
-        low = datetime_now + mean - (range_ / 2)
-        high = datetime_now + mean + (range_ / 2)
+        low = datetime_now + mean - (range_ // 2)
+        high = datetime_now + mean + (range_ // 2)
         self.assertThat(
             datetime.utcfromtimestamp(maintenance_call.getTime()),
             between(low, high),
@@ -313,7 +341,7 @@ class LeaseMaintenanceServiceTests(TestCase):
         # Figure out the absolute last run time.
         last_run = datetime_now - since_last_run
         last_run_path = FilePath(self.useFixture(TempDir()).join("last-run"))
-        last_run_path.setContent(last_run.isoformat())
+        last_run_path.setContent(ensure_binary(last_run.isoformat()))
 
         service = lease_maintenance_service(
             dummy_maintain_leases,
@@ -331,14 +359,14 @@ class LeaseMaintenanceServiceTests(TestCase):
 
         low = datetime_now + max(
             timedelta(0),
-            mean - (range_ / 2) - since_last_run,
+            mean - (range_ // 2) - since_last_run,
         )
         high = max(
             # If since_last_run is one microsecond (precision of timedelta)
             # then the range is indivisible.  Avoid putting the expected high
             # below the expected low.
             low,
-            datetime_now + mean + (range_ / 2) - since_last_run,
+            datetime_now + mean + (range_ // 2) - since_last_run,
         )
 
         note(
diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py
index 6c0e360cdd0ac810604fcb997fc394f4ee978d5b..0102ee70b46b69d95d1b45d08a522c7d14af1ac5 100644
--- a/src/_zkapauthorizer/tests/test_model.py
+++ b/src/_zkapauthorizer/tests/test_model.py
@@ -17,7 +17,34 @@
 Tests for ``_zkapauthorizer.model``.
 """
 
-from __future__ import absolute_import
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from future.utils import PY2
+
+# if PY2:
+#     from future.builtins import (  # noqa: F401
+#         filter,
+#         map,
+#         zip,
+#         ascii,
+#         chr,
+#         hex,
+#         input,
+#         next,
+#         oct,
+#         open,
+#         pow,
+#         round,
+#         super,
+#         bytes,
+#         dict,
+#         list,
+#         object,
+#         range,
+#         str,
+#         max,
+#         min,
+#     )
 
 from datetime import datetime, timedelta
 from errno import EACCES
@@ -141,8 +168,8 @@ class VoucherStoreTests(TestCase):
         """
         counter_a = counters[0]
         counter_b = counters[1]
-        tokens_a = tokens[: len(tokens) / 2]
-        tokens_b = tokens[len(tokens) / 2 :]
+        tokens_a = tokens[: len(tokens) // 2]
+        tokens_b = tokens[len(tokens) // 2 :]
 
         store = self.useFixture(TemporaryVoucherStore(get_config, lambda: now)).store
         # We only have to get the expected_tokens value (len(tokens)) right on
@@ -253,13 +280,13 @@ class VoucherStoreTests(TestCase):
         then ``VoucherStore.from_node_config`` raises ``StoreOpenError``.
         """
         tempdir = self.useFixture(TempDir())
-        nodedir = tempdir.join(b"node")
+        nodedir = tempdir.join(u"node")
 
         # Create the node directory without permission to create the
         # underlying directory.
         mkdir(nodedir, 0o500)
 
-        config = get_config(nodedir, b"tub.port")
+        config = get_config(nodedir, u"tub.port")
 
         self.assertThat(
             lambda: VoucherStore.from_node_config(
@@ -295,9 +322,9 @@ class VoucherStoreTests(TestCase):
         ``VoucherStore.from_node_config`` raises ``StoreOpenError``.
         """
         tempdir = self.useFixture(TempDir())
-        nodedir = tempdir.join(b"node")
+        nodedir = tempdir.join(u"node")
 
-        config = get_config(nodedir, b"tub.port")
+        config = get_config(nodedir, u"tub.port")
 
         # Create the underlying database file.
         store = VoucherStore.from_node_config(config, lambda: now)
@@ -358,9 +385,9 @@ class VoucherStoreTests(TestCase):
         :return: A three-tuple of (backed up tokens, extracted tokens, inserted tokens).
         """
         tempdir = self.useFixture(TempDir())
-        nodedir = tempdir.join(b"node")
+        nodedir = tempdir.join(u"node")
 
-        config = get_config(nodedir, b"tub.port")
+        config = get_config(nodedir, u"tub.port")
 
         # Create the underlying database file.
         store = VoucherStore.from_node_config(config, lambda: now)
@@ -384,14 +411,14 @@ class VoucherStoreTests(TestCase):
         while tokens_remaining > 0:
             to_spend = data.draw(integers(min_value=1, max_value=tokens_remaining))
             extracted_tokens.extend(
-                token.unblinded_token for token in store.get_unblinded_tokens(to_spend)
+                token.unblinded_token.decode("ascii") for token in store.get_unblinded_tokens(to_spend)
             )
             tokens_remaining -= to_spend
 
         return (
             backed_up_tokens,
             extracted_tokens,
-            list(token.unblinded_token for token in unblinded_tokens),
+            list(token.unblinded_token.decode("ascii") for token in unblinded_tokens),
         )
 
 
@@ -946,7 +973,7 @@ def store_for_test(testcase, get_config, get_now):
     :return VoucherStore: A newly created temporary store.
     """
     tempdir = testcase.useFixture(TempDir())
-    config = get_config(tempdir.join(b"node"), b"tub.port")
+    config = get_config(tempdir.join(u"node"), u"tub.port")
     store = VoucherStore.from_node_config(
         config,
         get_now,
diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py
index 831f4236734cbce2c2300c612881137bd111709b..0baa11245d08f93ad0ba0d32dfbdde655f0d43a1 100644
--- a/src/_zkapauthorizer/tests/test_plugin.py
+++ b/src/_zkapauthorizer/tests/test_plugin.py
@@ -48,6 +48,7 @@ if PY2:
 from datetime import timedelta
 from functools import partial
 from os import makedirs
+import os.path
 
 from allmydata.client import config_from_string, create_client_from_config
 from allmydata.interfaces import (
@@ -66,7 +67,8 @@ from hypothesis import given, settings
 from hypothesis.strategies import datetimes, just, sampled_from, timedeltas
 from prometheus_client import Gauge
 from prometheus_client.parser import text_string_to_metric_families
-from StringIO import StringIO
+from six.moves import StringIO
+from six import ensure_binary
 from testtools import TestCase
 from testtools.content import text_content
 from testtools.matchers import (
@@ -389,8 +391,8 @@ class ClientPluginTests(TestCase):
         """
         tempdir = self.useFixture(TempDir())
         node_config = get_config(
-            tempdir.join(b"node"),
-            b"tub.port",
+            tempdir.join(u"node"),
+            u"tub.port",
         )
 
         storage_client = storage_server.get_storage_client(
@@ -412,8 +414,8 @@ class ClientPluginTests(TestCase):
         """
         tempdir = self.useFixture(TempDir())
         node_config = config_from_string(
-            tempdir.join(b"node"),
-            b"tub.port",
+            tempdir.join(u"node"),
+            u"tub.port",
             config_text.encode("utf-8"),
         )
         # On Tahoe-LAFS <1.16, the config is written as bytes.
@@ -466,8 +468,8 @@ class ClientPluginTests(TestCase):
         """
         tempdir = self.useFixture(TempDir())
         node_config = get_config(
-            tempdir.join(b"node"),
-            b"tub.port",
+            tempdir.join(u"node"),
+            u"tub.port",
         )
 
         storage_client = storage_server.get_storage_client(
@@ -516,8 +518,8 @@ class ClientPluginTests(TestCase):
         """
         tempdir = self.useFixture(TempDir())
         node_config = get_config(
-            tempdir.join(b"node"),
-            b"tub.port",
+            tempdir.join(u"node"),
+            u"tub.port",
         )
 
         store = VoucherStore.from_node_config(node_config, lambda: now)
@@ -548,12 +550,12 @@ class ClientPluginTests(TestCase):
         # tests, at least until creating a real server doesn't involve so much
         # complex setup.  So avoid using any of the client APIs that make a
         # remote call ... which is all of them.
-        pass_group = storage_client._get_passes("request binding message", num_passes)
+        pass_group = storage_client._get_passes(b"request binding message", num_passes)
         pass_group.mark_spent()
 
         # There should be no unblinded tokens left to extract.
         self.assertThat(
-            lambda: storage_client._get_passes("request binding message", 1),
+            lambda: storage_client._get_passes(b"request binding message", 1),
             raises(NotEnoughTokens),
         )
 
@@ -567,7 +569,7 @@ class ClientPluginTests(TestCase):
                         lambda logged_message: logged_message.message,
                         ContainsDict(
                             {
-                                "message": Equals("request binding message"),
+                                "message": Equals(u"request binding message"),
                                 "count": Equals(num_passes),
                             }
                         ),
@@ -589,8 +591,8 @@ class ClientResourceTests(TestCase):
         ``get_client_resource`` returns an object that provides ``IResource``.
         """
         tempdir = self.useFixture(TempDir())
-        nodedir = tempdir.join(b"node")
-        config = get_config(nodedir, b"tub.port")
+        nodedir = tempdir.join(u"node")
+        config = get_config(nodedir, u"tub.port")
         self.assertThat(
             storage_server.get_client_resource(
                 config,
@@ -652,20 +654,28 @@ class LeaseMaintenanceServiceTests(TestCase):
             file, ``False`` otherwise.
         """
         tempdir = self.useFixture(TempDir())
-        nodedir = tempdir.join(b"node")
-        privatedir = tempdir.join(b"node", b"private")
+        nodedir = tempdir.join(u"node")
+        privatedir = tempdir.join(u"node", u"private")
         makedirs(privatedir)
-        config = get_config(nodedir, b"tub.port")
+        config = get_config(nodedir, u"tub.port")
+
+        # In Tahoe-LAFS 1.17 write_private_config is broken.  It mixes bytes
+        # and unicode in an os.path.join() call that always fails with a
+        # TypeError.
+        def write_private_config(name, value):
+            privname = os.path.join(config._basedir, u"private", name)
+            with open(privname, "wb") as f:
+                f.write(value)
 
         if servers_yaml is not None:
             # Provide it a statically configured server to connect to.
-            config.write_private_config(
-                b"servers.yaml",
+            write_private_config(
+                u"servers.yaml",
                 servers_yaml,
             )
         if rootcap:
             config.write_private_config(
-                b"rootcap",
+                u"rootcap",
                 b"dddddddd",
             )
 
@@ -780,7 +790,7 @@ class LoadSigningKeyTests(TestCase):
 
         :param bytes key: A base64-encoded Ristretto signing key.
         """
-        p = FilePath(self.useFixture(TempDir()).join(b"key"))
+        p = FilePath(self.useFixture(TempDir()).join(u"key"))
         p.setContent(key_bytes)
         key = load_signing_key(p)
         self.assertThat(key, IsInstance(SigningKey))
diff --git a/src/_zkapauthorizer/tests/test_private.py b/src/_zkapauthorizer/tests/test_private.py
index 568cc1eb1baf613c17ee3336874b08b9aed4b17c..effc6dc73021ae53fa56c612896c109cb1f4d89c 100644
--- a/src/_zkapauthorizer/tests/test_private.py
+++ b/src/_zkapauthorizer/tests/test_private.py
@@ -10,6 +10,32 @@ Tests for ``_zkapauthorizer.private``.
 """
 
 from __future__ import absolute_import, division, print_function, unicode_literals
+from future.utils import PY2
+if  PY2:
+    from future.builtins import (  # noqa: F401
+        filter,
+        map,
+        zip,
+        ascii,
+        chr,
+        hex,
+        input,
+        next,
+        oct,
+        open,
+        pow,
+        round,
+        super,
+        bytes,
+        dict,
+        list,
+        object,
+        range,
+        str,
+        max,
+        min,
+    )
+
 
 from allmydata.test.web.matchers import has_response_code
 from testtools import TestCase
@@ -39,7 +65,9 @@ class PrivacyTests(TestCase):
     def _authorization(self, scheme, value):
         return Headers(
             {
-                "authorization": ["{} {}".format(scheme, value)],
+                u"authorization": [
+                    u"{} {}".format(scheme.decode("ascii"), value.decode("ascii")),
+                ],
             }
         )
 
@@ -60,7 +88,7 @@ class PrivacyTests(TestCase):
         self.assertThat(
             self.client.head(
                 b"http:///foo/bar",
-                headers=self._authorization("basic", self.token),
+                headers=self._authorization(b"basic", self.token),
             ),
             succeeded(has_response_code(Equals(UNAUTHORIZED))),
         )
@@ -73,7 +101,7 @@ class PrivacyTests(TestCase):
         self.assertThat(
             self.client.head(
                 b"http:///foo/bar",
-                headers=self._authorization(SCHEME, "foo bar"),
+                headers=self._authorization(SCHEME, b"foo bar"),
             ),
             succeeded(has_response_code(Equals(UNAUTHORIZED))),
         )
diff --git a/src/_zkapauthorizer/tests/test_spending.py b/src/_zkapauthorizer/tests/test_spending.py
index 6833e0895e5d8f4ec38310eec5c25456ccc8e601..55f9aa9276eff0a3ab3ec3062cd743637a264743 100644
--- a/src/_zkapauthorizer/tests/test_spending.py
+++ b/src/_zkapauthorizer/tests/test_spending.py
@@ -16,6 +16,35 @@
 Tests for ``_zkapauthorizer.spending``.
 """
 
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from future.utils import PY2
+
+if  PY2:
+    from future.builtins import (  # noqa: F401
+        filter,
+        map,
+        zip,
+        ascii,
+        chr,
+        hex,
+        input,
+        next,
+        oct,
+        open,
+        pow,
+        round,
+        super,
+        bytes,
+        dict,
+        list,
+        object,
+        range,
+        str,
+        max,
+        min,
+    )
+
 from hypothesis import given
 from hypothesis.strategies import data, integers, randoms
 from testtools import TestCase
@@ -62,7 +91,7 @@ class PassGroupTests(TestCase):
             store=configless.store,
         )
 
-        group = pass_factory.get(u"message", num_passes)
+        group = pass_factory.get(b"message", num_passes)
         self.assertThat(
             group,
             MatchesAll(
@@ -97,7 +126,7 @@ class PassGroupTests(TestCase):
         # Figure out some subset, maybe empty, of passes from the group that
         # we will try to operate on.
         group_size = data.draw(integers(min_value=0, max_value=num_passes))
-        indices = range(num_passes)
+        indices = list(range(num_passes))
         random.shuffle(indices)
         spent_indices = indices[:group_size]
 
@@ -106,7 +135,7 @@ class PassGroupTests(TestCase):
             tokens_to_passes=configless.redeemer.tokens_to_passes,
             store=configless.store,
         )
-        group = pass_factory.get(u"message", num_passes)
+        group = pass_factory.get(b"message", num_passes)
         spent, rest = group.split(spent_indices)
         operation(spent)
 
diff --git a/src/_zkapauthorizer/tests/test_storage_client.py b/src/_zkapauthorizer/tests/test_storage_client.py
index 1075884f570d7b15b220f5d33843c5105ecb522f..8da2975c2fb89abb482d4eaee04cbb99be846b0e 100644
--- a/src/_zkapauthorizer/tests/test_storage_client.py
+++ b/src/_zkapauthorizer/tests/test_storage_client.py
@@ -16,7 +16,34 @@
 Tests for ``_zkapauthorizer._storage_client``.
 """
 
-from __future__ import division
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from future.utils import PY2
+
+if  PY2:
+    from future.builtins import (  # noqa: F401
+        filter,
+        map,
+        zip,
+        ascii,
+        chr,
+        hex,
+        input,
+        next,
+        oct,
+        open,
+        pow,
+        round,
+        super,
+        bytes,
+        dict,
+        list,
+        object,
+        range,
+        str,
+        max,
+        min,
+    )
 
 from functools import partial
 
@@ -180,7 +207,7 @@ class CallWithPassesTests(TestCase):
             call_with_passes(
                 lambda group: succeed(result),
                 num_passes,
-                partial(pass_factory(integer_passes(num_passes)).get, u"message"),
+                partial(pass_factory(integer_passes(num_passes)).get, b"message"),
             ),
             succeeded(Is(result)),
         )
@@ -197,7 +224,7 @@ class CallWithPassesTests(TestCase):
             call_with_passes(
                 lambda group: fail(result),
                 num_passes,
-                partial(pass_factory(integer_passes(num_passes)).get, u"message"),
+                partial(pass_factory(integer_passes(num_passes)).get, b"message"),
             ),
             failed(
                 AfterPreprocessing(
@@ -220,7 +247,7 @@ class CallWithPassesTests(TestCase):
             call_with_passes(
                 lambda group: succeed(group.passes),
                 num_passes,
-                partial(passes.get, u"message"),
+                partial(passes.get, b"message"),
             ),
             succeeded(
                 Equals(
@@ -241,7 +268,7 @@ class CallWithPassesTests(TestCase):
             call_with_passes(
                 lambda group: None,
                 num_passes,
-                partial(passes.get, u"message"),
+                partial(passes.get, b"message"),
             ),
             succeeded(Always()),
         )
@@ -261,7 +288,7 @@ class CallWithPassesTests(TestCase):
             call_with_passes(
                 lambda group: fail(Exception("Anything")),
                 num_passes,
-                partial(passes.get, u"message"),
+                partial(passes.get, b"message"),
             ),
             failed(Always()),
         )
@@ -301,7 +328,7 @@ class CallWithPassesTests(TestCase):
             call_with_passes(
                 reject_even_pass_values,
                 num_passes,
-                partial(passes.get, u"message"),
+                partial(passes.get, b"message"),
             ),
             succeeded(Always()),
         )
@@ -342,7 +369,7 @@ class CallWithPassesTests(TestCase):
             call_with_passes(
                 reject_passes,
                 num_passes,
-                partial(passes.get, u"message"),
+                partial(passes.get, b"message"),
             ),
             failed(
                 AfterPreprocessing(
@@ -406,7 +433,7 @@ class CallWithPassesTests(TestCase):
                 # out of passes no matter how many we start with.
                 reject_half_passes,
                 num_passes,
-                partial(passes.get, u"message"),
+                partial(passes.get, b"message"),
             ),
             failed(
                 AfterPreprocessing(
@@ -458,7 +485,7 @@ class PassFactoryTests(TestCase):
         ``IPassGroup.reset`` makes passes available to be returned by
         ``IPassGroup.get`` again.
         """
-        message = u"message"
+        message = b"message"
         min_passes = min(num_passes_a, num_passes_b)
         max_passes = max(num_passes_a, num_passes_b)
 
@@ -486,7 +513,7 @@ class PassFactoryTests(TestCase):
         :param (IPassGroup -> None) invalid_op: Some follow-up operation to
             perform with the pass group and to assert raises an exception.
         """
-        message = u"message"
+        message = b"message"
         factory = pass_factory(integer_passes(num_passes))
         group = factory.get(message, num_passes)
         setup_op(group)
diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py
index 89654633b6b9176ec98fb71492571b0467eb42b7..ffae2fff58e545382b3ca2b701ce2f1cd10ac993 100644
--- a/src/_zkapauthorizer/tests/test_storage_protocol.py
+++ b/src/_zkapauthorizer/tests/test_storage_protocol.py
@@ -235,14 +235,14 @@ class ShareTests(TestCase):
 
         # Make some passes with a key untrusted by the server.
         bad_passes = get_passes(
-            allocate_buckets_message(storage_index),
+            allocate_buckets_message(storage_index).encode("utf-8"),
             len(bad_pass_indexes),
             random_signing_key(),
         )
 
         # Make some passes with a key trusted by the server.
         good_passes = get_passes(
-            allocate_buckets_message(storage_index),
+            allocate_buckets_message(storage_index).encode("utf-8"),
             num_passes - len(bad_passes),
             self.signing_key,
         )
diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py
index 7e334d79e6f8fca00f16170d46dca9b2bd1f1513..b40e82633a4fa45772bd48a34e607c0614807289 100644
--- a/src/_zkapauthorizer/tests/test_storage_server.py
+++ b/src/_zkapauthorizer/tests/test_storage_server.py
@@ -81,7 +81,7 @@ class ValidationResultTests(TestCase):
         ``validate_passes`` returns a ``_ValidationResult`` instance which
         describes the valid and invalid passes.
         """
-        message = u"hello world"
+        message = b"hello world"
         valid_passes = get_passes(
             message,
             valid_count,
@@ -136,7 +136,7 @@ class ValidationResultTests(TestCase):
                     AfterPreprocessing(
                         str,
                         Equals(
-                            "MorePassesRequired(valid_count=4, required_count=10, signature_check_failed=frozenset([4]))"
+                            "MorePassesRequired(valid_count=4, required_count=10, signature_check_failed={})".format(str(frozenset([4]))),
                         ),
                     ),
                 ),
@@ -243,7 +243,7 @@ class PassValidationTests(TestCase):
         renew_secret = b"x" * 32
         cancel_secret = b"y" * 32
         valid_passes = get_passes(
-            allocate_buckets_message(storage_index),
+            allocate_buckets_message(storage_index).encode("utf-8"),
             required_passes - 1,
             self.signing_key,
         )
@@ -345,7 +345,7 @@ class PassValidationTests(TestCase):
         )
 
         valid_passes = get_passes(
-            slot_testv_and_readv_and_writev_message(storage_index),
+            slot_testv_and_readv_and_writev_message(storage_index).encode("utf-8"),
             required_pass_count,
             self.signing_key,
         )
@@ -481,7 +481,7 @@ class PassValidationTests(TestCase):
             tw_vectors,
         )
         valid_passes = get_passes(
-            slot_testv_and_readv_and_writev_message(storage_index),
+            slot_testv_and_readv_and_writev_message(storage_index).encode("utf-8"),
             required_pass_count,
             self.signing_key,
         )
@@ -550,7 +550,7 @@ class PassValidationTests(TestCase):
 
         # Attempt the lease operation with one fewer pass than is required.
         passes = get_passes(
-            add_lease_message(storage_index),
+            add_lease_message(storage_index).encode("utf-8"),
             required_count - 1,
             self.signing_key,
         )
@@ -614,7 +614,7 @@ class PassValidationTests(TestCase):
             tw_vectors,
         )
         valid_passes = get_passes(
-            slot_testv_and_readv_and_writev_message(slot),
+            slot_testv_and_readv_and_writev_message(slot).encode("utf-8"),
             required_pass_count,
             self.signing_key,
         )
@@ -678,7 +678,7 @@ class PassValidationTests(TestCase):
             tw_vectors,
         )
         valid_passes = get_passes(
-            slot_testv_and_readv_and_writev_message(storage_index),
+            slot_testv_and_readv_and_writev_message(storage_index).encode("utf-8"),
             num_passes,
             self.signing_key,
         )
@@ -739,7 +739,7 @@ class PassValidationTests(TestCase):
             tw_vectors,
         )
         valid_passes = get_passes(
-            slot_testv_and_readv_and_writev_message(storage_index),
+            slot_testv_and_readv_and_writev_message(storage_index).encode("utf-8"),
             num_passes,
             self.signing_key,
         )
@@ -820,7 +820,7 @@ class PassValidationTests(TestCase):
             [size] * len(new_sharenums - existing_sharenums),
         )
         valid_passes = get_passes(
-            allocate_buckets_message(storage_index),
+            allocate_buckets_message(storage_index).encode("utf-8"),
             num_passes,
             self.signing_key,
         )
@@ -887,7 +887,7 @@ class PassValidationTests(TestCase):
             self.storage_server._pass_value, [allocated_size] * len(sharenums)
         )
         valid_passes = get_passes(
-            add_lease_message(storage_index),
+            add_lease_message(storage_index).encode("utf-8"),
             num_passes,
             self.signing_key,
         )
@@ -948,7 +948,7 @@ class PassValidationTests(TestCase):
             self.storage_server._pass_value, [allocated_size] * len(sharenums)
         )
         valid_passes = get_passes(
-            add_lease_message(storage_index),
+            add_lease_message(storage_index).encode("utf-8"),
             num_passes,
             self.signing_key,
         )
diff --git a/src/_zkapauthorizer/tests/test_strategies.py b/src/_zkapauthorizer/tests/test_strategies.py
index b046450cc8289561a141b1e1e62b3979d4638692..da9757050a89655d1545218b150ab69b3c73fdc8 100644
--- a/src/_zkapauthorizer/tests/test_strategies.py
+++ b/src/_zkapauthorizer/tests/test_strategies.py
@@ -16,7 +16,34 @@
 Tests for our custom Hypothesis strategies.
 """
 
-from __future__ import absolute_import
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+from future.utils import PY2
+
+if PY2:
+    from future.builtins import (  # noqa: F401
+        filter,
+        map,
+        zip,
+        ascii,
+        chr,
+        hex,
+        input,
+        next,
+        oct,
+        open,
+        pow,
+        round,
+        super,
+        bytes,
+        dict,
+        list,
+        object,
+        range,
+        str,
+        max,
+        min,
+    )
 
 from allmydata.client import config_from_string
 from fixtures import TempDir
@@ -49,7 +76,7 @@ class TahoeConfigsTests(TestCase):
         )
         note(config_text)
         config_from_string(
-            tempdir.join(b"tahoe.ini"),
-            b"tub.port",
+            tempdir.join(u"tahoe.ini"),
+            u"tub.port",
             config_text.encode("utf-8"),
         )