diff --git a/.circleci/populate-wheelhouse.sh b/.circleci/populate-wheelhouse.sh
index 80b684eba..75afb6f6f 100755
--- a/.circleci/populate-wheelhouse.sh
+++ b/.circleci/populate-wheelhouse.sh
@@ -40,7 +40,7 @@ export PIP_FIND_LINKS="file://${WHEELHOUSE_PATH}"
 "${PIP}" \
     wheel \
     --wheel-dir "${WHEELHOUSE_PATH}" \
-    "${PROJECT_ROOT}"[test,tor,i2p] \
+    "${PROJECT_ROOT}"[test] \
     ${BASIC_DEPS} \
     ${TEST_DEPS} \
     ${REPORTING_DEPS}
diff --git a/docs/INSTALL.rst b/docs/INSTALL.rst
index f85d5e124..5f9d72d1d 100644
--- a/docs/INSTALL.rst
+++ b/docs/INSTALL.rst
@@ -193,6 +193,17 @@ You can also install directly from the source tarball URL::
  tahoe-lafs: 1.13.0
  ...
 
+Extras
+------
+
+Tahoe-LAFS provides some functionality only when explicitly requested at installation time.
+It does this using the "extras" feature of setuptools.
+You can request these extra features when running the ``pip install`` command like this::
+
+  % venv/bin/pip install tahoe-lafs[tor]
+
+This example enables support for listening and connecting using Tor.
+The Tahoe-LAFS documentation for specific features which require an explicit install-time step will mention the "extra" that must be requested.
 
 Hacking On Tahoe-LAFS
 ---------------------
diff --git a/newsfragments/3240.minor b/newsfragments/3240.minor
new file mode 100644
index 000000000..e69de29bb
diff --git a/setup.py b/setup.py
index 357b9cd41..f3b837d1d 100644
--- a/setup.py
+++ b/setup.py
@@ -75,12 +75,21 @@ install_requires = [
     #   leftover timers)
     # * Twisted-16.4.0 introduces `python -m twisted.trial` which is needed
     #   for coverage testing
-
     # * Twisted 16.6.0 drops the undesirable gmpy dependency from the conch
     #   extra, letting us use that extra instead of trying to duplicate its
     #   dependencies here.  Twisted[conch] >18.7 introduces a dependency on
     #   bcrypt.  It is nice to avoid that if the user ends up with an older
     #   version of Twisted.  That's hard to express except by using the extra.
+    #
+    #   In a perfect world, Twisted[conch] would be a dependency of an "sftp"
+    #   extra.  However, pip fails to resolve the dependencies all
+    #   dependencies when asked for Twisted[tls] *and* Twisted[conch].
+    #   Specifically, "Twisted[conch]" (as the later requirement) is ignored.
+    #   If there were an Tahoe-LAFS sftp extra that dependended on
+    #   Twisted[conch] and install_requires only included Twisted[tls] then
+    #   `pip install tahoe-lafs[sftp]` would not install requirements
+    #   specified by Twisted[conch].  Since this would be the *whole point* of
+    #   an sftp extra in Tahoe-LAFS, there is no point in having one.
     "Twisted[tls,conch] >= 16.6.0",
 
     # We need Nevow >= 0.11.1 which can be installed using pip.
@@ -108,6 +117,18 @@ setup_requires = [
     'setuptools >= 28.8.0',  # for PEP-440 style versions
 ]
 
+tor_requires = [
+    # This is exactly what `foolscap[tor]` means but pip resolves the pair of
+    # dependencies "foolscap[i2p] foolscap[tor]" to "foolscap[i2p]" so we lose
+    # this if we don't declare it ourselves!
+    "txtorcon >= 0.17.0",
+]
+
+i2p_requires = [
+    # See the comment in tor_requires.
+    "txi2p >= 0.3.2",
+]
+
 if len(sys.argv) > 1 and sys.argv[1] == '--fakedependency':
     del sys.argv[1]
     install_requires += ["fakedependency >= 1.0.0"]
@@ -330,10 +351,6 @@ setup(name="tahoe-lafs", # also set in __init__.py
               "coverage",
               "mock",
               "tox",
-              "foolscap[tor] >= 0.12.5",
-              "txtorcon >= 0.17.0", # in case pip's resolver doesn't work
-              "foolscap[i2p] >= 0.12.6",
-              "txi2p >= 0.3.2", # in case pip's resolver doesn't work
               "pytest",
               "pytest-twisted",
               "hypothesis >= 3.6.1",
@@ -341,15 +358,9 @@ setup(name="tahoe-lafs", # also set in __init__.py
               "towncrier",
               "testtools",
               "fixtures",
-          ],
-          "tor": [
-              "foolscap[tor] >= 0.12.5",
-              "txtorcon >= 0.17.0", # in case pip's resolver doesn't work
-          ],
-          "i2p": [
-              "foolscap[i2p] >= 0.12.6",
-              "txi2p >= 0.3.2", # in case pip's resolver doesn't work
-          ],
+          ] + tor_requires + i2p_requires,
+          "tor": tor_requires,
+          "i2p": i2p_requires,
       },
       package_data={"allmydata.web": ["*.xhtml",
                                       "static/*.js", "static/*.png", "static/*.css",
diff --git a/src/allmydata/test/test_sftp.py b/src/allmydata/test/test_sftp.py
index 06b6d1d7a..b6f1fbc8a 100644
--- a/src/allmydata/test/test_sftp.py
+++ b/src/allmydata/test/test_sftp.py
@@ -12,18 +12,15 @@ from allmydata.util import deferredutil
 conch_interfaces = None
 sftp = None
 sftpd = None
-have_pycrypto = False
-try:
-    from Crypto import Util
-    Util  # hush pyflakes
-    have_pycrypto = True
-except ImportError:
-    pass
 
-if have_pycrypto:
+try:
     from twisted.conch import interfaces as conch_interfaces
     from twisted.conch.ssh import filetransfer as sftp
     from allmydata.frontends import sftpd
+except ImportError as e:
+    conch_unavailable_reason = e
+else:
+    conch_unavailable_reason = None
 
 from allmydata.interfaces import IDirectoryNode, ExistingChildError, NoSuchChildError
 from allmydata.mutable.common import NotWriteableError
@@ -38,8 +35,10 @@ from allmydata.test.common_util import ReallyEqualMixin
 class Handler(GridTestMixin, ShouldFailMixin, ReallyEqualMixin, unittest.TestCase):
     """This is a no-network unit test of the SFTPUserHandler and the abstractions it uses."""
 
-    if not have_pycrypto:
-        skip = "SFTP support requires pycrypto, which is not installed"
+    if conch_unavailable_reason:
+        skip = "SFTP support requires Twisted Conch which is not available: {}".format(
+            conch_unavailable_reason,
+        )
 
     def shouldFailWithSFTPError(self, expected_code, which, callable, *args, **kwargs):
         assert isinstance(expected_code, int), repr(expected_code)