diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst
index b403363d6d25b7e8c6d4c84c871c3c8b11865ecf..7701ffa1e4cfcab8542b276eda1f5c8e4f8b3e1a 100644
--- a/docs/source/configuration.rst
+++ b/docs/source/configuration.rst
@@ -48,3 +48,12 @@ Then also configure the Ristretto-flavored PrivacyPass issuer the server will an
 
   [storageserver.plugins.privatestorageio-zkapauthz-v1]
   ristretto-issuer-root-url = https://issuer.example.invalid/
+
+The storage server must also be configured with the path to the Ristretto-flavored PrivacyPass signing key.
+To avoid placing secret material in tahoe.cfg,
+this configuration is done using a path::
+
+  [storageserver.plugins.privatestorageio-zkapauthz-v1]
+  ristretto-signing-key-path = /path/to/signing.key
+
+The signing key is the keystone secret to the entire system and must be managed with extreme care to prevent unintended disclosure.
diff --git a/setup.py b/setup.py
index 606849326a4002007fd42060b51e69a19c18675c..f61c0015abc4af74c07c67a1f6127d032867aa66 100644
--- a/setup.py
+++ b/setup.py
@@ -1,3 +1,7 @@
 from setuptools import setup
 
-setup()
+setup(
+    package_data={
+        "": ["testing-signing.key"],
+    },
+)
diff --git a/src/_zkapauthorizer/_plugin.py b/src/_zkapauthorizer/_plugin.py
index e5c59e23456605c95c36ed24839aa1a439957520..355642dbf1ae232099ae802fba3520b573b555a1 100644
--- a/src/_zkapauthorizer/_plugin.py
+++ b/src/_zkapauthorizer/_plugin.py
@@ -27,6 +27,9 @@ from zope.interface import (
     implementer,
 )
 
+from twisted.python.filepath import (
+    FilePath,
+)
 from twisted.internet.defer import (
     succeed,
 )
@@ -35,6 +38,9 @@ from allmydata.interfaces import (
     IFoolscapStoragePlugin,
     IAnnounceableStorageServer,
 )
+from privacypass import (
+    SigningKey,
+)
 
 from .api import (
     ZKAPAuthorizerStorageServer,
@@ -104,11 +110,17 @@ class ZKAPAuthorizer(object):
     def get_storage_server(self, configuration, get_anonymous_storage_server):
         kwargs = configuration.copy()
         root_url = kwargs.pop(u"ristretto-issuer-root-url")
+        signing_key = SigningKey.decode_base64(
+            FilePath(
+                kwargs.pop(u"ristretto-signing-key-path"),
+            ).getContent(),
+        )
         announcement = {
             u"ristretto-issuer-root-url": root_url,
         }
         storage_server = ZKAPAuthorizerStorageServer(
             get_anonymous_storage_server(),
+            signing_key,
             **kwargs
         )
         return succeed(
diff --git a/src/_zkapauthorizer/_storage_client.py b/src/_zkapauthorizer/_storage_client.py
index 7b200ba3789e98e3b38b64811986485aea2e4896..364aeac907f97ab94663cebf65401a10c94262ce 100644
--- a/src/_zkapauthorizer/_storage_client.py
+++ b/src/_zkapauthorizer/_storage_client.py
@@ -30,6 +30,13 @@ from allmydata.interfaces import (
     IStorageServer,
 )
 
+from .storage_common import (
+    allocate_buckets_message,
+    add_lease_message,
+    renew_lease_message,
+    slot_testv_and_readv_and_writev_message,
+)
+
 @implementer(IStorageServer)
 @attr.s
 class ZKAPAuthorizerStorageClient(object):
@@ -64,13 +71,16 @@ class ZKAPAuthorizerStorageClient(object):
 
     def _get_encoded_passes(self, message, count):
         """
+        :param unicode message: The message to which to bind the passes.
+
         :return: A list of passes from ``_get_passes`` encoded into their
             ``bytes`` representation.
         """
+        assert isinstance(message, unicode)
         return list(
             t.text.encode("ascii")
             for t
-            in self._get_passes(message.encode("hex"), count)
+            in self._get_passes(message.encode("utf-8"), count)
         )
 
     def get_version(self):
@@ -89,7 +99,7 @@ class ZKAPAuthorizerStorageClient(object):
     ):
         return self._rref.callRemote(
             "allocate_buckets",
-            self._get_encoded_passes(storage_index, 1),
+            self._get_encoded_passes(allocate_buckets_message(storage_index), 1),
             storage_index,
             renew_secret,
             cancel_secret,
@@ -115,7 +125,7 @@ class ZKAPAuthorizerStorageClient(object):
     ):
         return self._rref.callRemote(
             "add_lease",
-            self._get_encoded_passes(storage_index, 1),
+            self._get_encoded_passes(add_lease_message(storage_index), 1),
             storage_index,
             renew_secret,
             cancel_secret,
@@ -128,7 +138,7 @@ class ZKAPAuthorizerStorageClient(object):
     ):
         return self._rref.callRemote(
             "renew_lease",
-            self._get_encoded_passes(storage_index, 1),
+            self._get_encoded_passes(renew_lease_message(storage_index), 1),
             storage_index,
             renew_secret,
         )
@@ -157,7 +167,7 @@ class ZKAPAuthorizerStorageClient(object):
     ):
         return self._rref.callRemote(
             "slot_testv_and_readv_and_writev",
-            self._get_encoded_passes(storage_index, 1),
+            self._get_encoded_passes(slot_testv_and_readv_and_writev_message(storage_index), 1),
             storage_index,
             secrets,
             tw_vectors,
diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py
index 313821647146fae8d4647384c5866455fa73ca73..e40a00d1ea6731a55cea4916048543e4dd843d99 100644
--- a/src/_zkapauthorizer/_storage_server.py
+++ b/src/_zkapauthorizer/_storage_server.py
@@ -27,12 +27,12 @@ from __future__ import (
 import attr
 from attr.validators import (
     provides,
+    instance_of,
 )
 
 from zope.interface import (
     implementer_only,
 )
-
 from foolscap.api import (
     Referenceable,
 )
@@ -43,10 +43,20 @@ from foolscap.ipb import (
 from allmydata.interfaces import (
     RIStorageServer,
 )
-
+from privacypass import (
+    TokenPreimage,
+    VerificationSignature,
+    SigningKey,
+)
 from .foolscap import (
     RITokenAuthorizedStorageServer,
 )
+from .storage_common import (
+    allocate_buckets_message,
+    add_lease_message,
+    renew_lease_message,
+    slot_testv_and_readv_and_writev_message,
+)
 
 @implementer_only(RITokenAuthorizedStorageServer, IReferenceable, IRemotelyCallable)
 # It would be great to use `frozen=True` (value-based hashing) instead of
@@ -59,18 +69,46 @@ class ZKAPAuthorizerStorageServer(Referenceable):
     before allowing certain functionality.
     """
     _original = attr.ib(validator=provides(RIStorageServer))
+    _signing_key = attr.ib(validator=instance_of(SigningKey))
 
-    def _validate_passes(self, passes):
+    def _is_invalid_pass(self, message, pass_):
         """
-        Check that all of the given passes are valid.
+        Check the validity of a single pass.
 
-        :raise InvalidPass: If any pass in ``passses`` is not valid.
+        :param unicode message: The shared message for pass validation.
+        :param bytes pass_: The encoded pass to validate.
 
-        :return NoneType: If all of the passes in ``passes`` are valid.
+        :return bool: ``True`` if the pass is invalid, ``False`` otherwise.
+        """
+        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)
+            unblinded_token = self._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"))
+            return invalid_pass
+        except Exception:
+            # It would be pretty nice to log something here, sometimes, I guess?
+            return True
 
-        :note: This is yet to be implemented so it always returns ``None``.
+    def _validate_passes(self, message, passes):
         """
-        return None
+        Check all of the given passes for validity.
+
+        :param unicode message: The shared message for pass validation.
+        :param list[bytes] passes: The encoded passes to validate.
+
+        :return list[bytes]: The passes which are found to be valid.
+        """
+        return list(
+            pass_
+            for pass_
+            in passes
+            if not self._is_invalid_pass(message, pass_)
+        )
 
     def remote_get_version(self):
         """
@@ -79,13 +117,13 @@ class ZKAPAuthorizerStorageServer(Referenceable):
         """
         return self._original.remote_get_version()
 
-    def remote_allocate_buckets(self, passes, *a, **kw):
+    def remote_allocate_buckets(self, passes, storage_index, *a, **kw):
         """
         Pass-through after a pass check to ensure that clients can only allocate
         storage for immutable shares if they present valid passes.
         """
-        self._validate_passes(passes)
-        return self._original.remote_allocate_buckets(*a, **kw)
+        self._validate_passes(allocate_buckets_message(storage_index), passes)
+        return self._original.remote_allocate_buckets(storage_index, *a, **kw)
 
     def remote_get_buckets(self, storage_index):
         """
@@ -94,21 +132,21 @@ class ZKAPAuthorizerStorageServer(Referenceable):
         """
         return self._original.remote_get_buckets(storage_index)
 
-    def remote_add_lease(self, passes, *a, **kw):
+    def remote_add_lease(self, passes, storage_index, *a, **kw):
         """
         Pass-through after a pass check to ensure clients can only extend the
         duration of share storage if they present valid passes.
         """
-        self._validate_passes(passes)
-        return self._original.remote_add_lease(*a, **kw)
+        self._validate_passes(add_lease_message(storage_index), passes)
+        return self._original.remote_add_lease(storage_index, *a, **kw)
 
-    def remote_renew_lease(self, passes, *a, **kw):
+    def remote_renew_lease(self, passes, storage_index, *a, **kw):
         """
         Pass-through after a pass check to ensure clients can only extend the
         duration of share storage if they present valid passes.
         """
-        self._validate_passes(passes)
-        return self._original.remote_renew_lease(*a, **kw)
+        self._validate_passes(renew_lease_message(storage_index), passes)
+        return self._original.remote_renew_lease(storage_index, *a, **kw)
 
     def remote_advise_corrupt_share(self, *a, **kw):
         """
@@ -133,7 +171,7 @@ class ZKAPAuthorizerStorageServer(Referenceable):
             data in already-allocated storage.  These cases may not be the
             same from the perspective of pass validation.
         """
-        self._validate_passes(passes)
+        self._validate_passes(slot_testv_and_readv_and_writev_message(storage_index), passes)
         # Skip over the remotely exposed method and jump to the underlying
         # implementation which accepts one additional parameter that we know
         # about (and don't expose over the network): renew_leases.  We always
diff --git a/src/_zkapauthorizer/storage_common.py b/src/_zkapauthorizer/storage_common.py
new file mode 100644
index 0000000000000000000000000000000000000000..c22bbc2bee6574a6eca394ae6fa0aae552a67089
--- /dev/null
+++ b/src/_zkapauthorizer/storage_common.py
@@ -0,0 +1,17 @@
+
+from base64 import (
+    b64encode,
+)
+
+def _message_maker(label):
+    def make_message(storage_index):
+        return u"{label} {storage_index}".format(
+            label=label,
+            storage_index=b64encode(storage_index),
+        )
+    return make_message
+
+allocate_buckets_message = _message_maker(u"allocate_buckets")
+add_lease_message = _message_maker(u"add_lease")
+renew_lease_message = _message_maker(u"renew_lease")
+slot_testv_and_readv_and_writev_message = _message_maker(u"slot_testv_and_readv_and_writev")
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index 0af3d1f86dc76db4b2e2627eedde8972e85a2338..5a0c531a6448bda2c59d40deeb9aa9e3523be77e 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -164,12 +164,16 @@ def node_nicknames():
     )
 
 
-def server_configurations():
+def server_configurations(signing_key_path):
     """
     Build configuration values for the server-side plugin.
+
+    :param unicode signing_key_path: A value to insert for the
+        **ristretto-signing-key-path** item.
     """
     return just({
         u"ristretto-issuer-root-url": u"https://issuer.example.invalid/",
+        u"ristretto-signing-key-path": signing_key_path.path,
     })
 
 
@@ -214,13 +218,10 @@ def zkaps():
     """
     Build random ZKAPs as ``Pass` instances.
     """
-    return binary(
-        min_size=32,
-        max_size=32,
-    ).map(
-        urlsafe_b64encode,
-    ).map(
-        lambda zkap: Pass(zkap.decode("ascii")),
+    return builds(
+        lambda preimage, signature: Pass(u"{} {}".format(preimage, signature)),
+        preimage=binary(min_size=66, max_size=66).map(urlsafe_b64encode),
+        signature=binary(min_size=66, max_size=66).map(urlsafe_b64encode),
     )
 
 
diff --git a/src/_zkapauthorizer/tests/test_plugin.py b/src/_zkapauthorizer/tests/test_plugin.py
index b8acfc58a6c463198f1210d859d72d344d03bc5c..0257683da1813e697407d9336ffdd4880ad62147 100644
--- a/src/_zkapauthorizer/tests/test_plugin.py
+++ b/src/_zkapauthorizer/tests/test_plugin.py
@@ -71,6 +71,9 @@ from allmydata.interfaces import (
     RIStorageServer,
 )
 
+from twisted.python.filepath import (
+    FilePath,
+)
 from twisted.plugin import (
     getPlugins,
 )
@@ -107,6 +110,9 @@ from .matchers import (
 )
 
 
+SIGNING_KEY_PATH = FilePath(__file__).sibling(u"testing-signing.key")
+
+
 @implementer(RIStorageServer)
 class StubStorageServer(object):
     pass
@@ -150,7 +156,7 @@ class ServerPluginTests(TestCase):
     Tests for the plugin's implementation of
     ``IFoolscapStoragePlugin.get_storage_server``.
     """
-    @given(server_configurations())
+    @given(server_configurations(SIGNING_KEY_PATH))
     def test_returns_announceable(self, configuration):
         """
         ``storage_server.get_storage_server`` returns an instance which provides
@@ -166,7 +172,7 @@ class ServerPluginTests(TestCase):
         )
 
 
-    @given(server_configurations())
+    @given(server_configurations(SIGNING_KEY_PATH))
     def test_returns_referenceable(self, configuration):
         """
         The storage server attached to the result of
@@ -187,7 +193,7 @@ class ServerPluginTests(TestCase):
             ),
         )
 
-    @given(server_configurations())
+    @given(server_configurations(SIGNING_KEY_PATH))
     def test_returns_serializable(self, configuration):
         """
         The storage server attached to the result of
@@ -211,7 +217,7 @@ class ServerPluginTests(TestCase):
         )
 
 
-    @given(server_configurations())
+    @given(server_configurations(SIGNING_KEY_PATH))
     def test_returns_hashable(self, configuration):
         """
         The storage server attached to the result of
diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py
index 0b3743db1ec1d642e9ef83148fb06fbf0eee1742..a32fa407129eee1288fc6cacbcaee44bbb0f5245 100644
--- a/src/_zkapauthorizer/tests/test_storage_protocol.py
+++ b/src/_zkapauthorizer/tests/test_storage_protocol.py
@@ -64,6 +64,10 @@ from foolscap.referenceable import (
     LocalReferenceable,
 )
 
+from privacypass import (
+    random_signing_key,
+)
+
 from .strategies import (
     storage_indexes,
     lease_renew_secrets,
@@ -92,6 +96,9 @@ from ..foolscap import (
 from ..model import (
     Pass,
 )
+from ..storage_common import (
+    slot_testv_and_readv_and_writev_message,
+)
 
 @attr.s
 class LocalRemote(object):
@@ -141,6 +148,7 @@ class ShareTests(TestCase):
         super(ShareTests, self).setUp()
         self.canary = LocalReferenceable(None)
         self.anonymous_storage_server = self.useFixture(AnonymousStorageServer()).storage_server
+        self.signing_key = random_signing_key()
 
         def get_passes(message, count):
             if not isinstance(message, bytes):
@@ -154,6 +162,7 @@ class ShareTests(TestCase):
 
         self.server = ZKAPAuthorizerStorageServer(
             self.anonymous_storage_server,
+            self.signing_key,
         )
         self.local_remote_server = LocalRemote(self.server)
         self.client = ZKAPAuthorizerStorageClient(
@@ -502,7 +511,10 @@ class ShareTests(TestCase):
         d = self.client._rref.callRemote(
             "slot_testv_and_readv_and_writev",
             # passes
-            self.client._get_encoded_passes(storage_index, 1),
+            self.client._get_encoded_passes(
+                slot_testv_and_readv_and_writev_message(storage_index),
+                1,
+            ),
             # storage_index
             storage_index,
             # secrets
diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py
new file mode 100644
index 0000000000000000000000000000000000000000..7be247f34efa305ee22e4d7b5120b7679592828a
--- /dev/null
+++ b/src/_zkapauthorizer/tests/test_storage_server.py
@@ -0,0 +1,123 @@
+from __future__ import (
+    absolute_import,
+)
+
+from random import (
+    shuffle,
+)
+from testtools import (
+    TestCase,
+)
+from testtools.matchers import (
+    Equals,
+    AfterPreprocessing,
+)
+from hypothesis import (
+    given,
+)
+from hypothesis.strategies import (
+    integers,
+    lists,
+)
+from privacypass import (
+    BatchDLEQProof,
+    PublicKey,
+    RandomToken,
+    random_signing_key,
+)
+
+from .strategies import (
+    zkaps,
+)
+from .fixtures import (
+    AnonymousStorageServer,
+)
+from ..api import (
+    ZKAPAuthorizerStorageServer,
+)
+
+
+def make_passes(signing_key, for_message, random_tokens):
+    blinded_tokens = list(
+        token.blind()
+        for token
+        in random_tokens
+    )
+    signatures = list(
+        signing_key.sign(blinded_token)
+        for blinded_token
+        in blinded_tokens
+    )
+    proof = BatchDLEQProof.create(
+        signing_key,
+        blinded_tokens,
+        signatures,
+    )
+    unblinded_signatures = proof.invalid_or_unblind(
+        random_tokens,
+        blinded_tokens,
+        signatures,
+        PublicKey.from_signing_key(signing_key),
+    )
+    preimages = list(
+        unblinded_signature.preimage()
+        for unblinded_signature
+        in unblinded_signatures
+    )
+    verification_keys = list(
+        unblinded_signature.derive_verification_key_sha512()
+        for unblinded_signature
+        in unblinded_signatures
+    )
+    message_signatures = list(
+        verification_key.sign_sha512(for_message.encode("utf-8"))
+        for verification_key
+        in verification_keys
+    )
+    passes = list(
+        u"{} {}".format(
+            preimage.encode_base64().decode("ascii"),
+            signature.encode_base64().decode("ascii"),
+        ).encode("ascii")
+        for (preimage, signature)
+        in zip(preimages, message_signatures)
+    )
+    return passes
+
+
+
+class PassValidationTests(TestCase):
+    """
+    Tests for pass validation performed by ``ZKAPAuthorizerStorageServer``.
+    """
+    def setUp(self):
+        super(PassValidationTests, self).setUp()
+        self.anonymous_storage_server = self.useFixture(AnonymousStorageServer()).storage_server
+        self.signing_key = random_signing_key()
+        self.storage_server = ZKAPAuthorizerStorageServer(
+            self.anonymous_storage_server,
+            self.signing_key,
+        )
+
+    @given(integers(min_value=0, max_value=64), lists(zkaps(), max_size=64))
+    def test_validation_result(self, valid_count, invalid_passes):
+        """
+        ``_get_valid_passes`` returns the number of cryptographically valid passes
+        in the list passed to it.
+        """
+        message = u"hello world"
+        valid_passes = make_passes(
+            self.signing_key,
+            message,
+            list(RandomToken.create() for i in range(valid_count)),
+        )
+        all_passes = valid_passes + list(pass_.text.encode("ascii") for pass_ in invalid_passes)
+        shuffle(all_passes)
+
+        self.assertThat(
+            self.storage_server._validate_passes(message, all_passes),
+            AfterPreprocessing(
+                set,
+                Equals(set(valid_passes)),
+            ),
+        )
diff --git a/src/_zkapauthorizer/tests/testing-signing.key b/src/_zkapauthorizer/tests/testing-signing.key
new file mode 100644
index 0000000000000000000000000000000000000000..078e2134bd719832938b965b0288e4246c364d2d
--- /dev/null
+++ b/src/_zkapauthorizer/tests/testing-signing.key
@@ -0,0 +1 @@
+mkQf85V2vyLQRUYuqRb+Ke6K+M9pOtXm4MslsuCdBgg=
\ No newline at end of file