diff --git a/src/_secureaccesstokenauthorizer/_storage_client.py b/src/_secureaccesstokenauthorizer/_storage_client.py
index 0648ab190bfd665d00ff502c396beb57144f49bd..4560c06266646e19c7b2e14095789de000ff6dd6 100644
--- a/src/_secureaccesstokenauthorizer/_storage_client.py
+++ b/src/_secureaccesstokenauthorizer/_storage_client.py
@@ -67,3 +67,12 @@ class SecureAccessTokenAuthorizerStorageClient(object):
             allocated_size,
             canary,
         )
+
+    def get_buckets(
+            self,
+            storage_index,
+    ):
+        return self._rref.callRemote(
+            "get_buckets",
+            storage_index,
+        )
diff --git a/src/_secureaccesstokenauthorizer/_storage_server.py b/src/_secureaccesstokenauthorizer/_storage_server.py
index 5593c289273d59c12c7f32a0e25f2c843008e439..7e4abcbccf5316e00972b2dc6c5b333cfd6c6ba3 100644
--- a/src/_secureaccesstokenauthorizer/_storage_server.py
+++ b/src/_secureaccesstokenauthorizer/_storage_server.py
@@ -20,14 +20,12 @@ This is the server part of a storage access protocol.  The client part is
 implemented in ``_storage_client.py``.
 """
 
+import attr
+
 from zope.interface import (
     implementer_only,
 )
 
-from twisted.python.components import (
-    proxyForInterface,
-)
-
 from foolscap.constraint import (
     ByteStringConstraint,
 )
@@ -103,14 +101,23 @@ class RITokenAuthorizedStorageServer(RemoteInterface):
 
 
 @implementer_only(RITokenAuthorizedStorageServer, IReferenceable, IRemotelyCallable)
-class SecureAccessTokenAuthorizerStorageServer(proxyForInterface(RIStorageServer), Referenceable):
-    def allocate_buckets(self, tokens, *a, **kw):
+@attr.s
+class SecureAccessTokenAuthorizerStorageServer(Referenceable):
+    _original = attr.ib()
+
+    def _validate_tokens(self, tokens):
+        pass
+
+    def remote_allocate_buckets(self, tokens, *a, **kw):
         self._validate_tokens(tokens)
-        return super(SecureAccessTokenAuthorizerStorageServer, self).allocate_buckets(*a, **kw)
+        return self._original.remote_allocate_buckets(*a, **kw)
+
+    def remote_get_buckets(self, storage_index):
+        return self._original.remote_get_buckets(storage_index)
 
-    def add_lease(self, tokens, *a, **kw):
+    def remote_add_lease(self, tokens, *a, **kw):
         self._validate_tokens(tokens)
-        return super(SecureAccessTokenAuthorizerStorageServer, self).add_lease(*a, **kw)
+        return self._original.remote_allocate_buckets(*a, **kw)
 
 # I don't understand why this is required.
 # SecureAccessTokenAuthorizerStorageServer is-a Referenceable.  It seems like
diff --git a/src/_secureaccesstokenauthorizer/tests/strategies.py b/src/_secureaccesstokenauthorizer/tests/strategies.py
index eb008c7767d77edc061f550523a2475e30acdfe4..945ad614e0a1ed8f3c4022008e641e3e983f74e3 100644
--- a/src/_secureaccesstokenauthorizer/tests/strategies.py
+++ b/src/_secureaccesstokenauthorizer/tests/strategies.py
@@ -18,6 +18,15 @@ Hypothesis strategies for property testing.
 
 from hypothesis.strategies import (
     just,
+    binary,
+    integers,
+    sets,
+)
+
+from allmydata.interfaces import (
+    StorageIndex,
+    LeaseRenewSecret,
+    LeaseCancelSecret,
 )
 
 def configurations():
@@ -25,3 +34,65 @@ def configurations():
     Build configuration values for the plugin.
     """
     return just({})
+
+
+def storage_indexes():
+    """
+    Build Tahoe-LAFS storage indexes.
+    """
+    return binary(
+        min_size=StorageIndex.minLength,
+        max_size=StorageIndex.maxLength,
+    )
+
+
+def lease_renew_secrets():
+    """
+    Build Tahoe-LAFS lease renewal secrets.
+    """
+    return binary(
+        min_size=LeaseRenewSecret.minLength,
+        max_size=LeaseRenewSecret.maxLength,
+    )
+
+
+def lease_cancel_secrets():
+    """
+    Build Tahoe-LAFS lease cancellation secrets.
+    """
+    return binary(
+        min_size=LeaseCancelSecret.minLength,
+        max_size=LeaseCancelSecret.maxLength,
+    )
+
+
+def sharenums():
+    """
+    Build Tahoe-LAFS share numbers.
+    """
+    return integers(
+        min_value=0,
+        max_value=255,
+    )
+
+
+def sharenum_sets():
+    """
+    Build sets of Tahoe-LAFS share numbers.
+    """
+    return sets(
+        sharenums(),
+        min_size=1,
+        max_size=255,
+    )
+
+
+def sizes():
+    """
+    Build Tahoe-LAFS share sizes.
+    """
+    return integers(
+        min_value=0,
+        # Just for practical purposes...
+        max_value=2 ** 16,
+    )
diff --git a/src/_secureaccesstokenauthorizer/tests/test_storage_protocol.py b/src/_secureaccesstokenauthorizer/tests/test_storage_protocol.py
new file mode 100644
index 0000000000000000000000000000000000000000..e19d67725d869fd770f702340f12e1bbbec5631f
--- /dev/null
+++ b/src/_secureaccesstokenauthorizer/tests/test_storage_protocol.py
@@ -0,0 +1,168 @@
+# Copyright 2019 PrivateStorage.io, LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Tests for communication between the client and server components.
+"""
+import attr
+
+from fixtures import (
+    Fixture,
+    TempDir,
+)
+from testtools import (
+    TestCase,
+)
+from testtools.matchers import (
+    Equals,
+)
+from testtools.twistedsupport._deferred import (
+    # I'd rather use https://twistedmatrix.com/trac/ticket/8900 but efforts
+    # there appear to have stalled.
+    extract_result,
+)
+
+from hypothesis import (
+    given,
+)
+
+from twisted.internet.defer import (
+    execute,
+)
+
+from foolscap.referenceable import (
+    LocalReferenceable,
+)
+
+from allmydata.storage.server import (
+    StorageServer,
+)
+
+from .strategies import (
+    storage_indexes,
+    lease_renew_secrets,
+    lease_cancel_secrets,
+    sharenum_sets,
+    sizes,
+)
+
+from ..api import (
+    SecureAccessTokenAuthorizerStorageServer,
+    SecureAccessTokenAuthorizerStorageClient,
+)
+
+def bytes_for_share(sharenum, size):
+    """
+    Generate marginally distinctive bytes of a certain length for the given
+    share number.
+    """
+    if 0 <= sharenum <= 255:
+        return (unichr(sharenum) * size).encode("latin-1")
+    raise ValueError("Sharenum must be between 0 and 255 inclusive.")
+
+
+class AnonymousStorageServer(Fixture):
+    def _setUp(self):
+        self.tempdir = self.useFixture(TempDir()).join(b"storage")
+        self.storage_server = StorageServer(
+            self.tempdir,
+            b"x" * 20,
+        )
+
+
+@attr.s
+class LocalRemote(object):
+    _referenceable = attr.ib()
+
+    def callRemote(self, methname, *args, **kwargs):
+        return execute(
+            getattr(self._referenceable, "remote_" + methname),
+            *args,
+            **kwargs
+        )
+
+
+class ImmutableTests(TestCase):
+    """
+    Tests for interaction with immutable shares.
+    """
+    @given(
+        storage_index=storage_indexes(),
+        renew_secret=lease_renew_secrets(),
+        cancel_secret=lease_cancel_secrets(),
+        sharenums=sharenum_sets(),
+        size=sizes(),
+    )
+    def test_create(self, storage_index, renew_secret, cancel_secret, sharenums, size):
+        """
+        Immutable share data created using *allocate_buckets* and methods of the
+        resulting buckets can be read back using *get_buckets* and methods of
+        those resulting buckets.
+        """
+        anonymous_storage_server = self.useFixture(AnonymousStorageServer()).storage_server
+
+        def get_tokens():
+            return [u"x"]
+
+        server = SecureAccessTokenAuthorizerStorageServer(
+            anonymous_storage_server,
+        )
+        local_remote_server = LocalRemote(server)
+        client = SecureAccessTokenAuthorizerStorageClient(
+            get_rref=lambda: local_remote_server,
+            get_tokens=get_tokens,
+        )
+
+        alreadygot, allocated = extract_result(
+            client.allocate_buckets(
+                storage_index,
+                renew_secret,
+                cancel_secret,
+                sharenums,
+                size,
+                canary=LocalReferenceable(None),
+            ),
+        )
+        self.expectThat(
+            alreadygot,
+            Equals(set()),
+            u"fresh server somehow already had shares",
+        )
+        self.expectThat(
+            set(allocated.keys()),
+            Equals(sharenums),
+            u"fresh server refused to allocate all requested buckets",
+        )
+
+        for sharenum, bucket in allocated.items():
+            # returns None, nothing to extract
+            bucket.remote_write(0, bytes_for_share(sharenum, size)),
+            # returns None, nothing to extract
+            bucket.remote_close()
+
+        readers = extract_result(client.get_buckets(storage_index))
+
+        self.expectThat(
+            set(readers.keys()),
+            Equals(sharenums),
+            u"server did not return all buckets we wrote",
+        )
+        for (sharenum, bucket) in readers.items():
+            self.expectThat(
+                bucket.remote_read(0, size),
+                Equals(bytes_for_share(sharenum, size)),
+                u"server returned wrong bytes for share number {}".format(
+                    sharenum,
+                ),
+            )