diff --git a/.circleci/config.yml b/.circleci/config.yml
index 9cd2b20cbaae07e50e7cea351c05ed72183d46b4..ae156c793dd77f514f21c158fbb99b409bb8ff92 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -53,7 +53,6 @@ jobs:
       - image: "nixorg/nix:circleci"
 
     environment:
-      CODECOV_TOKEN: "cc6e4697-4337-4506-88af-92b8f8ca6b22"
       # Specify a revision of NixOS/nixpkgs to run against.  This essentially
       # pins the majority of the software involved in the build.  This
       # revision is selected arbitrarily.  It's somewhat current as of the
@@ -144,7 +143,7 @@ jobs:
       - run:
           name: "Report Coverage"
           command: |
-            nix-shell -p 'python.withPackages (ps: [ ps.codecov ])' --keep CODECOV_TOKEN --run \
+            nix-shell -p 'python.withPackages (ps: [ ps.codecov ])' --run \
               'codecov --file ./result-doc/share/doc/*/.coverage'
 
 workflows:
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000000000000000000000000000000000000..ec19cb88f397c327bac1afa6a195073f305e3e3d
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,6 @@
+[run]
+source =
+    _zkapauthorizer
+    twisted.plugins.zkapauthorizer
+
+branch = True
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..0b6af690f7c47e638d89a962ad98f8fa01c479d1
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,77 @@
+name: "ci"
+
+on:
+  - "push"
+
+jobs:
+  unit-tests:
+    runs-on: "windows-latest"
+    strategy:
+      matrix:
+        python-version:
+          - "2.7"
+
+    steps:
+    # Avoid letting Windows newlines confusing milksnake.
+    - run: "git config --global core.autocrlf false"
+
+    - uses: actions/checkout@v2
+      with:
+        fetch-depth: "0"
+
+      # Get tags not fetched by the checkout action, needed for auto-versioning.
+    - run: "git fetch origin +refs/tags/*:refs/tags/*"
+
+    # Get MS VC++ 9 aka Visual Studio 2008, required to build Python 2.7
+    # extensions (zfec via Tahoe-LAFS).
+    - uses: "crazy-max/ghaction-chocolatey@v1"
+      with:
+        args: "install vcpython27"
+
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v1
+      with:
+        python-version: ${{ matrix.python-version }}
+
+    - name: "Upgrade Pip"
+      run: |
+        python -m pip install --upgrade pip
+
+    - name: "Install CI Dependencies"
+      run: |
+        python -m pip install wheel coverage
+
+    - name: "Install PrivacyPass master@HEAD"
+      run: |
+        python -m pip install git+https://github.com/LeastAuthority/privacypass@master#egg=privacypass
+
+    - name: "Install Tahoe-LAFS master@HEAD"
+      run: |
+        python -m pip install git+https://github.com/tahoe-lafs/tahoe-lafs@master#egg=tahoe-lafs
+
+    - name: "Install Test Dependencies"
+      run: |
+        python -m pip install -r test-requirements.txt
+
+    - name: "Install ZKAPAuthorizer"
+      run: |
+        python -m pip install ./
+
+    - name: "Dump Python Environment"
+      run: |
+        pip freeze
+
+    - name: "Run Tests"
+      env:
+        MAGIC_FOLDER_HYPOTHESIS_PROFILE: "ci"
+      run: |
+        python -m coverage run -m twisted.trial _zkapauthorizer
+
+
+    - name: "Convert Coverage"
+      run: |
+        coverage xml
+
+    - uses: codecov/codecov-action@v1
+      with:
+        file: "./coverage.xml"
diff --git a/codecov.yml b/codecov.yml
new file mode 100644
index 0000000000000000000000000000000000000000..40b059fae03e3f7df4c945b9c5ceb9e5983b1d08
--- /dev/null
+++ b/codecov.yml
@@ -0,0 +1,2 @@
+codecov:
+  token: "cc6e4697-4337-4506-88af-92b8f8ca6b22"
diff --git a/setup.cfg b/setup.cfg
index c35d4a8495fbc3b6fc4086c05fa1b148c8867463..659b2061f2a7287abe7c405f454363626f5930e5 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -35,7 +35,8 @@ install_requires =
     zope.interface
     aniso8601
     privacypass
-    twisted
+    # Inherit our Twisted dependency from tahoe-lafs so we don't accidentally
+    # get the extras wrong here and break stuff.
     tahoe-lafs
     treq
 
diff --git a/src/_zkapauthorizer/_storage_server.py b/src/_zkapauthorizer/_storage_server.py
index c06dea97b62eff7bd897f2cc6fb01e6ed129e40d..8c832827a38d0f44a023140b5b256eb6d852ce33 100644
--- a/src/_zkapauthorizer/_storage_server.py
+++ b/src/_zkapauthorizer/_storage_server.py
@@ -26,6 +26,7 @@ from __future__ import (
 
 from struct import (
     unpack,
+    calcsize,
 )
 
 from errno import (
@@ -507,10 +508,23 @@ def get_storage_index_share_size(sharepath):
 
     :return int: The data size of the share in bytes.
     """
-    with open(sharepath) as share_file:
-        share_data_length_bytes = share_file.read(8)[4:]
-        (share_data_length,) = unpack('>L', share_data_length_bytes)
-        return share_data_length
+    # Note Tahoe-LAFS immutable/layout.py makes some claims about how the
+    # share data is structured.  A lot of this seems to be wrong.
+    # storage/immutable.py appears to have the correct information.
+    fmt = ">LL"
+    with open(sharepath, "rb") as share_file:
+        header = share_file.read(calcsize(fmt))
+
+    if len(header) != calcsize(fmt):
+        raise ValueError(
+            "Tried to read {} bytes of share file header, got {!r} instead.".format(
+                calcsize(fmt),
+                header,
+            ),
+        )
+
+    version, share_data_length = unpack(fmt, header)
+    return share_data_length
 
 
 def get_lease_expiration(get_leases, storage_index_or_slot):
@@ -556,7 +570,7 @@ def get_slot_share_size(sharepath):
 
     :return int: The data size of the share in bytes.
     """
-    with open(sharepath) as share_file:
+    with open(sharepath, "rb") as share_file:
         share_data_length_bytes = share_file.read(92)[-8:]
         (share_data_length,) = unpack('>Q', share_data_length_bytes)
         return share_data_length
@@ -584,7 +598,7 @@ def get_stat(sharepath):
     This is necessary to differentiate between buckets and slots.
     """
     # Figure out if it is a storage index or a slot.
-    with open(sharepath) as share_file:
+    with open(sharepath, "rb") as share_file:
         magic = share_file.read(32)
         if magic == "Tahoe mutable container v1\n" + "\x75\x09\x44\x03\x8e":
             return stat_slot
diff --git a/src/_zkapauthorizer/tests/__init__.py b/src/_zkapauthorizer/tests/__init__.py
index 1fd9a9da7f721537646cd78176a3c063909b2101..0f9529a87a1b9837c7b34798450815918ee5a9f5 100644
--- a/src/_zkapauthorizer/tests/__init__.py
+++ b/src/_zkapauthorizer/tests/__init__.py
@@ -28,24 +28,36 @@ def _configure_hypothesis():
         settings,
     )
 
-    settings.register_profile(
-        "ci",
+    base = dict(
         suppress_health_check=[
-            # CPU resources available to CI builds typically varies
-            # significantly from run to run making it difficult to determine
-            # if "too slow" data generation is a result of the code or the
-            # execution environment.  Prevent these checks from
-            # (intermittently) failing tests that are otherwise fine.
+            # CPU resources available to builds typically varies significantly
+            # from run to run making it difficult to determine if "too slow"
+            # data generation is a result of the code or the execution
+            # environment.  Prevent these checks from (intermittently) failing
+            # tests that are otherwise fine.
             HealthCheck.too_slow,
         ],
         # With the same reasoning, disable the test deadline.
         deadline=None,
     )
 
+    settings.register_profile(
+        "default",
+        **base
+    )
+
+    settings.register_profile(
+        "ci",
+        # Make CI runs a little more aggressive in amount of coverage they try
+        # to provide.
+        max_examples=200,
+        **base
+    )
+
     settings.register_profile(
         "big",
         max_examples=10000,
-        deadline=None,
+        **base
     )
 
     profile_name = environ.get("ZKAPAUTHORIZER_HYPOTHESIS_PROFILE", "default")
diff --git a/src/_zkapauthorizer/tests/common.py b/src/_zkapauthorizer/tests/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..39aa9119e10c22670c2948b4f53a7dbb22dc4f6e
--- /dev/null
+++ b/src/_zkapauthorizer/tests/common.py
@@ -0,0 +1,41 @@
+# 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.
+
+"""
+Testing functionality that is specifically related to the test harness
+itself.
+"""
+
+
+def skipIf(condition, reason):
+    """
+    Create a decorate a function to be skipped if the given condition is true.
+
+    :param bool condition: The condition under which to skip.
+    :param unicode reason: A reason for the skip.
+
+    :return: A function decorator which will skip the test if the given
+        condition is true.
+    """
+    if condition:
+        return _skipper(reason)
+    return lambda x: x
+
+
+def _skipper(reason):
+    def wrapper(f):
+        def skipIt(self, *a, **kw):
+            self.skipTest(reason)
+        return skipIt
+    return wrapper
diff --git a/src/_zkapauthorizer/tests/strategies.py b/src/_zkapauthorizer/tests/strategies.py
index a4f4eadd80644a13a045015aa8fbf21d0f3cc043..0da7adf84af23d599a7ef8f4556d9d5a28bd2c10 100644
--- a/src/_zkapauthorizer/tests/strategies.py
+++ b/src/_zkapauthorizer/tests/strategies.py
@@ -488,10 +488,10 @@ def sizes():
     return integers(
         # Size 0 data isn't data, it's nothing.
         min_value=1,
-        # For the moment there are some assumptions in the test suite that
-        # limit us to an amount of storage that can be paid for with one ZKAP.
-        # That will be fixed eventually.  For now, keep the sizes pretty low.
-        max_value=2 ** 16,
+        # Let this be larger than a single segment (2 ** 17) in case that
+        # matters to Tahoe-LAFS storage at all.  I don't think it does,
+        # though.
+        max_value=2 ** 18,
     )
 
 
diff --git a/src/_zkapauthorizer/tests/test_model.py b/src/_zkapauthorizer/tests/test_model.py
index 9e7b779f7a842022437085b7082b5732873b2b3f..348c0cf0f2258389d9516eb0fa1da1b5e9bb86be 100644
--- a/src/_zkapauthorizer/tests/test_model.py
+++ b/src/_zkapauthorizer/tests/test_model.py
@@ -31,6 +31,10 @@ from datetime import (
     timedelta,
 )
 
+from unittest import (
+    skipIf,
+)
+
 from testtools import (
     TestCase,
 )
@@ -61,6 +65,9 @@ from hypothesis.strategies import (
     integers,
 )
 
+from twisted.python.runtime import (
+    platform,
+)
 from twisted.python.filepath import (
     FilePath,
 )
@@ -182,6 +189,7 @@ class VoucherStoreTests(TestCase):
             )),
         )
 
+    @skipIf(platform.isWindows(), "Hard to prevent directory creation on Windows")
     @given(tahoe_configs(), datetimes())
     def test_uncreateable_store_directory(self, get_config, now):
         """
@@ -222,6 +230,7 @@ class VoucherStoreTests(TestCase):
         )
 
 
+    @skipIf(platform.isWindows(), "Hard to prevent database from being opened on Windows")
     @given(tahoe_configs(), datetimes())
     def test_unopenable_store(self, get_config, now):
         """
diff --git a/src/_zkapauthorizer/tests/test_storage_protocol.py b/src/_zkapauthorizer/tests/test_storage_protocol.py
index 7d9de02808dde0a547ded59ad4cd3592ff5fdcb1..569f852561669862a88ca277254822d3b93df3b3 100644
--- a/src/_zkapauthorizer/tests/test_storage_protocol.py
+++ b/src/_zkapauthorizer/tests/test_storage_protocol.py
@@ -54,6 +54,9 @@ from hypothesis.strategies import (
     integers,
 )
 
+from twisted.python.runtime import (
+    platform,
+)
 from twisted.python.filepath import (
     FilePath,
 )
@@ -67,6 +70,10 @@ from privacypass import (
     random_signing_key,
 )
 
+from .common import (
+    skipIf,
+)
+
 from .privacypass import (
     make_passes,
 )
@@ -151,7 +158,6 @@ class RequiredPassesTests(TestCase):
         )
 
 
-
 class ShareTests(TestCase):
     """
     Tests for interaction with shares.
@@ -395,6 +401,7 @@ class ShareTests(TestCase):
         )
 
 
+    @skipIf(platform.isWindows(), "Storage server miscomputes slot size on Windows")
     @given(
         storage_index=storage_indexes(),
         secrets=tuples(
@@ -462,6 +469,10 @@ class ShareTests(TestCase):
         )
 
 
+    @skipIf(
+        platform.isWindows(),
+        "StorageServer fails to create necessary directory for corruption advisories in Windows.",
+    )
     @given(
         storage_index=storage_indexes(),
         renew_secret=lease_renew_secrets(),
diff --git a/src/_zkapauthorizer/tests/test_storage_server.py b/src/_zkapauthorizer/tests/test_storage_server.py
index 4cef248a8ae6b09f38d7968c220903fa45480b42..c00b4a9e04399f82b89069e5986e90c0ba6baf6e 100644
--- a/src/_zkapauthorizer/tests/test_storage_server.py
+++ b/src/_zkapauthorizer/tests/test_storage_server.py
@@ -27,6 +27,7 @@ from time import (
 from random import (
     shuffle,
 )
+
 from testtools import (
     TestCase,
 )
@@ -52,6 +53,9 @@ from privacypass import (
     random_signing_key,
 )
 
+from twisted.python.runtime import (
+    platform,
+)
 from twisted.internet.task import (
     Clock,
 )
@@ -60,6 +64,9 @@ from foolscap.referenceable import (
     LocalReferenceable,
 )
 
+from .common import (
+    skipIf,
+)
 from .privacypass import (
     make_passes,
 )
@@ -100,6 +107,7 @@ class PassValidationTests(TestCase):
     """
     Tests for pass validation performed by ``ZKAPAuthorizerStorageServer``.
     """
+    @skipIf(platform.isWindows(), "Storage server is not supported on Windows")
     def setUp(self):
         super(PassValidationTests, self).setUp()
         self.clock = Clock()
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..68868665b6a9b214a73a65c3376781d1eda91cc6
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,3 @@
+fixtures
+testtools
+hypothesis