diff --git a/make-keypairs/Main.hs b/make-keypairs/Main.hs
index d77171e5303c5fedfedabacef1830f9c687e5581..3e5eea01f14048b4fca3c12a2d041a2be5de5fa6 100644
--- a/make-keypairs/Main.hs
+++ b/make-keypairs/Main.hs
@@ -1,11 +1,9 @@
 module Main where
 
 import qualified Crypto.PubKey.RSA as RSA
-import Data.ASN1.BinaryEncoding (DER (DER))
-import Data.ASN1.Encoding (ASN1Encoding (encodeASN1))
-import Data.ASN1.Types (ASN1Object (toASN1))
-import qualified Data.ByteString.Lazy as LB
-import Data.X509 (PrivKey (PrivKeyRSA))
+import qualified Data.ByteString as B
+import Tahoe.SDMF.Internal.Keys (signatureKeyToBytes)
+import Tahoe.SDMF.Keys (Signature (..))
 
 -- | The size of the keys to generate.
 bits :: Int
@@ -21,8 +19,12 @@ main = do
 
 genKey :: Show a => a -> IO ()
 genKey n = do
-    (_, priv) <- RSA.generate bits e
-    let bytes = encodeASN1 DER (toASN1 (PrivKeyRSA priv) [])
-    LB.writeFile ("test/data/rsa-privkey-" <> show n <> ".der") bytes
+    print "Generating RSA key..."
+    (_, priv) <- RSA.generate (bits `div` 8) e
+    print $ "Serializing key " <> show n
+    let bytes = signatureKeyToBytes (Signature priv)
+    print $ "Generated them (" <> show (B.length bytes) <> " bytes)"
+    B.writeFile ("test/data/rsa-privkey-" <> show n <> ".der") bytes
+    print "Wrote them to the file."
   where
     e = 0x10001
diff --git a/src/Tahoe/SDMF/Internal/Keys.hs b/src/Tahoe/SDMF/Internal/Keys.hs
new file mode 100644
index 0000000000000000000000000000000000000000..e40d6fbefa909e638a689f7d802a3b7ee53bc1d4
--- /dev/null
+++ b/src/Tahoe/SDMF/Internal/Keys.hs
@@ -0,0 +1,200 @@
+{- | Key types, derivations, and related functionality for SDMF.
+
+ See docs/specifications/mutable.rst for details.
+-}
+module Tahoe.SDMF.Internal.Keys where
+
+import Prelude hiding (Read)
+
+import Control.Monad (when)
+import Crypto.Cipher.AES (AES128)
+import Crypto.Cipher.Types (Cipher (cipherInit, cipherKeySize), IV, KeySizeSpecifier (KeySizeFixed))
+import Crypto.Error (maybeCryptoError)
+import qualified Crypto.PubKey.RSA as RSA
+import Crypto.Random (MonadRandom)
+import Data.ASN1.BinaryEncoding (DER (DER))
+import Data.ASN1.Encoding (ASN1Encoding (encodeASN1), decodeASN1')
+import Data.ASN1.Types (ASN1 (End, IntVal, Null, OID, OctetString, Start), ASN1ConstructionType (Sequence), ASN1Object (fromASN1, toASN1))
+import Data.Bifunctor (Bifunctor (first))
+import qualified Data.ByteArray as ByteArray
+import qualified Data.ByteString as B
+import Data.ByteString.Base32 (encodeBase32Unpadded)
+import qualified Data.ByteString.Lazy as LB
+import qualified Data.Text as T
+import Data.X509 (PrivKey (PrivKeyRSA), PubKey (PubKeyRSA))
+import Tahoe.CHK.Crypto (taggedHash, taggedPairHash)
+
+newtype KeyPair = KeyPair {toPrivateKey :: RSA.PrivateKey} deriving newtype (Show)
+
+toPublicKey :: KeyPair -> RSA.PublicKey
+toPublicKey = RSA.private_pub . toPrivateKey
+
+newtype Verification = Verification {unVerification :: RSA.PublicKey}
+newtype Signature = Signature {unSignature :: RSA.PrivateKey}
+    deriving newtype (Eq, Show)
+
+data Write = Write {unWrite :: AES128, writeKeyBytes :: ByteArray.ScrubbedBytes}
+instance Show Write where
+    show (Write _ bs) = T.unpack $ T.concat ["<WriteKey ", encodeBase32Unpadded (ByteArray.convert bs), ">"]
+
+data Read = Read {unRead :: AES128, readKeyBytes :: ByteArray.ScrubbedBytes}
+newtype StorageIndex = StorageIndex {unStorageIndex :: B.ByteString}
+
+newtype WriteEnablerMaster = WriteEnablerMaster ByteArray.ScrubbedBytes
+
+newtype WriteEnabler = WriteEnabler ByteArray.ScrubbedBytes
+
+data Data = Data {unData :: AES128, dataKeyBytes :: ByteArray.ScrubbedBytes}
+
+newtype SDMF_IV = SDMF_IV (IV AES128)
+    deriving (Eq)
+    deriving newtype (ByteArray.ByteArrayAccess)
+
+instance Show SDMF_IV where
+    show (SDMF_IV iv) = T.unpack . T.toLower . encodeBase32Unpadded . ByteArray.convert $ iv
+
+-- | The size of the public/private key pair to generate.
+keyPairBits :: Int
+keyPairBits = 2048
+
+keyLength :: Int
+(KeySizeFixed keyLength) = cipherKeySize (undefined :: AES128)
+
+{- | Create a new, random key pair (public/private aka verification/signature)
+ of the appropriate type and size for SDMF encryption.
+-}
+newKeyPair :: MonadRandom m => m KeyPair
+newKeyPair = do
+    (_, priv) <- RSA.generate keyPairBits e
+    pure $ KeyPair priv
+  where
+    e = 0x10001
+
+-- | Compute the write key for a given signature key for an SDMF share.
+deriveWriteKey :: Signature -> Maybe Write
+deriveWriteKey s =
+    Write <$> key <*> pure (ByteArray.convert sbs)
+  where
+    sbs = taggedHash keyLength mutableWriteKeyTag . signatureKeyToBytes $ s
+    key = maybeCryptoError . cipherInit $ sbs
+
+mutableWriteKeyTag :: B.ByteString
+mutableWriteKeyTag = "allmydata_mutable_privkey_to_writekey_v1"
+
+-- | Compute the read key for a given write key for an SDMF share.
+deriveReadKey :: Write -> Maybe Read
+deriveReadKey w =
+    Read <$> key <*> pure (ByteArray.convert sbs)
+  where
+    sbs = taggedHash keyLength mutableReadKeyTag . ByteArray.convert . writeKeyBytes $ w
+    key = maybeCryptoError . cipherInit $ sbs
+
+mutableReadKeyTag :: B.ByteString
+mutableReadKeyTag = "allmydata_mutable_writekey_to_readkey_v1"
+
+-- | Compute the data encryption/decryption key for a given read key for an SDMF share.
+deriveDataKey :: SDMF_IV -> Read -> Maybe Data
+deriveDataKey (SDMF_IV iv) r =
+    Data <$> key <*> pure (ByteArray.convert sbs)
+  where
+    -- XXX taggedPairHash has a bug where it doesn't ever truncate so we
+    -- truncate for it.
+    sbs = B.take keyLength . taggedPairHash keyLength mutableDataKeyTag (B.pack . ByteArray.unpack $ iv) . ByteArray.convert . readKeyBytes $ r
+    key = maybeCryptoError . cipherInit $ sbs
+
+mutableDataKeyTag :: B.ByteString
+mutableDataKeyTag = "allmydata_mutable_readkey_to_datakey_v1"
+
+-- | Compute the storage index for a given read key for an SDMF share.
+deriveStorageIndex :: Read -> StorageIndex
+deriveStorageIndex r = StorageIndex si
+  where
+    si = taggedHash keyLength mutableStorageIndexTag . ByteArray.convert . readKeyBytes $ r
+
+mutableStorageIndexTag :: B.ByteString
+mutableStorageIndexTag = "allmydata_mutable_readkey_to_storage_index_v1"
+
+{- | Derive the "write enabler master" secret for a given write key for an
+ SDMF share.
+-}
+deriveWriteEnablerMaster :: Write -> WriteEnablerMaster
+deriveWriteEnablerMaster w = WriteEnablerMaster bs
+  where
+    -- This one shouldn't be truncated.  Set the length to the size of sha256d
+    -- output.
+    bs = ByteArray.convert . taggedHash 32 mutableWriteEnablerMasterTag . ByteArray.convert . writeKeyBytes $ w
+
+mutableWriteEnablerMasterTag :: B.ByteString
+mutableWriteEnablerMasterTag = "allmydata_mutable_writekey_to_write_enabler_master_v1"
+
+{- | Derive the "write enabler" secret for a given peer and "write enabler
+ master" for an SDMF share.
+-}
+deriveWriteEnabler :: B.ByteString -> WriteEnablerMaster -> WriteEnabler
+deriveWriteEnabler peerid (WriteEnablerMaster master) = WriteEnabler bs
+  where
+    -- This one shouldn't be truncated.  Set the length to the size of sha256d
+    -- output.
+    bs = ByteArray.convert . taggedPairHash 32 mutableWriteEnablerTag (ByteArray.convert master) $ peerid
+
+mutableWriteEnablerTag :: B.ByteString
+mutableWriteEnablerTag = "allmydata_mutable_write_enabler_master_and_nodeid_to_write_enabler_v1"
+
+{- | Encode a public key to the Tahoe-LAFS canonical bytes representation -
+ X.509 SubjectPublicKeyInfo of the ASN.1 DER serialization of an RSA
+ PublicKey.
+-}
+verificationKeyToBytes :: Verification -> B.ByteString
+verificationKeyToBytes = LB.toStrict . encodeASN1 DER . flip toASN1 [] . PubKeyRSA . unVerification
+
+{- | Encode a private key to the Tahoe-LAFS canonical bytes representation -
+ X.509 SubjectPublicKeyInfo of the ASN.1 DER serialization of an RSA
+ PublicKey.
+-}
+signatureKeyToBytes :: Signature -> B.ByteString
+signatureKeyToBytes = LB.toStrict . encodeASN1 DER . toPKCS8
+  where
+    -- The ASN1Object instance for PrivKeyRSA can interpret an x509
+    -- "Private-Key Information" (aka PKCS8; see RFC 5208, section 5)
+    -- structure but it _produces_ some other format.  We must have exactly
+    -- this format.
+    --
+    -- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+    --
+    -- RFC 5208 says:
+    --
+    --    privateKey is an octet string whose contents are the value of the
+    --    private key.  The interpretation of the contents is defined in the
+    --    registration of the private-key algorithm.  For an RSA private key,
+    --    for example, the contents are a BER encoding of a value of type
+    --    RSAPrivateKey.
+    --
+    -- The ASN.1 BER encoding for a given structure is *not guaranteed to be
+    -- unique*.  This means that in general there is no guarantee of a unique
+    -- bytes representation of a signature key in this scheme so *key
+    -- derivations are not unique*.  If any two implementations disagree on
+    -- this encoding (which BER allows them to do) they will not interoperate.
+    toPKCS8 (Signature privKey) =
+        [ Start Sequence
+        , IntVal 0
+        , Start Sequence
+        , OID [1, 2, 840, 113549, 1, 1, 1]
+        , Null
+        , End Sequence
+        , -- Our ASN.1 encoder doesn't even pretend to support BER.  Use DER!
+          -- It results in the same bytes as Tahoe-LAFS is working with so ...
+          -- Maybe we're lucky or maybe Tahoe-LAFS isn't actually following
+          -- the spec.
+          OctetString (LB.toStrict . encodeASN1 DER . toASN1 (PrivKeyRSA privKey) $ [])
+        , End Sequence
+        ]
+
+-- | Decode a private key from the Tahoe-LAFS canonical bytes representation.
+signatureKeyFromBytes :: B.ByteString -> Either String Signature
+signatureKeyFromBytes bs = do
+    asn1s <- first show $ decodeASN1' DER bs
+    (key, extra) <- fromASN1 asn1s
+    when (extra /= []) (Left $ "left over data: " <> show extra)
+    case key of
+        (PrivKeyRSA privKey) -> Right $ Signature privKey
+        _ -> Left ("Expect RSA private key, found " <> show key)
diff --git a/src/Tahoe/SDMF/Internal/Share.hs b/src/Tahoe/SDMF/Internal/Share.hs
index 989f0714f57be13589de2ceb8fd6e3b36d9554b3..8d90b2aaf21576cde2a5d9116c1342187b0e4aeb 100644
--- a/src/Tahoe/SDMF/Internal/Share.hs
+++ b/src/Tahoe/SDMF/Internal/Share.hs
@@ -13,12 +13,11 @@ import Data.Binary.Get (bytesRead, getByteString, getLazyByteString, getRemainin
 import Data.Binary.Put (putByteString, putLazyByteString, putWord16be, putWord32be, putWord64be, putWord8)
 import qualified Data.ByteArray as ByteArray
 import qualified Data.ByteString as B
-import Data.ByteString.Base32 (encodeBase32Unpadded)
 import qualified Data.ByteString.Lazy as LB
-import qualified Data.Text as T
 import Data.Word (Word16, Word64, Word8)
 import Data.X509 (PubKey (PubKeyRSA))
 import Tahoe.CHK.Merkle (MerkleTree, leafHashes)
+import Tahoe.SDMF.Internal.Keys (SDMF_IV (..))
 
 hashSize :: Int
 hashSize = 32
@@ -89,13 +88,6 @@ data Share = Share
     }
     deriving (Eq, Show)
 
-newtype SDMF_IV = SDMF_IV (IV AES128)
-    deriving (Eq)
-    deriving newtype (ByteArray.ByteArrayAccess)
-
-instance Show SDMF_IV where
-    show (SDMF_IV iv) = T.unpack . T.toLower . encodeBase32Unpadded . ByteArray.convert $ iv
-
 instance Binary Share where
     put Share{..} = do
         putWord8 0
diff --git a/src/Tahoe/SDMF/Keys.hs b/src/Tahoe/SDMF/Keys.hs
new file mode 100644
index 0000000000000000000000000000000000000000..165915612de3689305539139b0c0e53807b323da
--- /dev/null
+++ b/src/Tahoe/SDMF/Keys.hs
@@ -0,0 +1,20 @@
+module Tahoe.SDMF.Keys (module Tahoe.SDMF.Internal.Keys) where
+
+import Tahoe.SDMF.Internal.Keys (
+    Data (..),
+    KeyPair (..),
+    Read (..),
+    SDMF_IV (..),
+    Signature (..),
+    StorageIndex (..),
+    Write (..),
+    WriteEnabler (..),
+    WriteEnablerMaster (..),
+    deriveDataKey,
+    deriveReadKey,
+    deriveStorageIndex,
+    deriveWriteEnabler,
+    deriveWriteEnablerMaster,
+    deriveWriteKey,
+    toPublicKey,
+ )
diff --git a/tahoe-ssk.cabal b/tahoe-ssk.cabal
index 7a873bea65afab04a5eabd94b1b6f4a776218664..d7e3e7b2fa120ba97e87eda4899e187d4bda37ad 100644
--- a/tahoe-ssk.cabal
+++ b/tahoe-ssk.cabal
@@ -1,4 +1,4 @@
-cabal-version:   2.4
+cabal-version:      2.4
 
 -- The cabal-version field refers to the version of the .cabal specification,
 -- and can be different from the cabal-install (the tool) version and the
@@ -12,7 +12,7 @@ cabal-version:   2.4
 --   http://haskell.org/cabal/users-guide/
 --
 -- The name of the package.
-name:            tahoe-ssk
+name:               tahoe-ssk
 
 -- The package version.
 -- See the Haskell package versioning policy (PVP) for standards
@@ -21,7 +21,7 @@ name:            tahoe-ssk
 -- PVP summary:     +-+------- breaking API changes
 --                  | | +----- non-breaking API additions
 --                  | | | +--- code changes with no API change
-version:         0.1.0.0
+version:            0.1.0.0
 
 -- A short (one-line) description of the package.
 synopsis:
@@ -31,30 +31,31 @@ synopsis:
 -- description:
 
 -- URL for the project homepage or repository.
-homepage:        https://whetstone.private.storage/PrivateStorage/tahoe-ssk
+homepage:           https://whetstone.private.storage/PrivateStorage/tahoe-ssk
 
 -- The license under which the package is released.
-license:         BSD-3-Clause
+license:            BSD-3-Clause
 
 -- The file containing the license text.
-license-file:    LICENSE
+license-file:       LICENSE
 
 -- The package author(s).
-author:          Jean-Paul Calderone
+author:             Jean-Paul Calderone
 
 -- An email address to which users can send suggestions, bug reports, and patches.
-maintainer:      jean-paul@private.storage
+maintainer:         jean-paul@private.storage
 
 -- A copyright notice.
 -- copyright:
-category:        Cryptography,Library,Parsers,Security
-build-type:      Simple
+category:           Cryptography,Library,Parsers,Security
+build-type:         Simple
 
 -- Extra doc files to be distributed with the package, such as a CHANGELOG or a README.
-extra-doc-files: CHANGELOG.md
+extra-doc-files:    CHANGELOG.md
 
--- Extra source files to be distributed with the package, such as examples, or a tutorial module.
--- extra-source-files:
+-- Extra source files to be distributed with the package, such as examples, or
+-- a tutorial module.  In our case, test data.
+extra-source-files: test/data/*
 
 common warnings
   ghc-options: -Wall
@@ -63,7 +64,9 @@ library
   hs-source-dirs:     src
   exposed-modules:
     Tahoe.SDMF
+    Tahoe.SDMF.Internal.Keys
     Tahoe.SDMF.Internal.Share
+    Tahoe.SDMF.Keys
 
   build-depends:
     , asn1-encoding
@@ -120,15 +123,18 @@ test-suite tahoe-ssk-test
     , asn1-encoding
     , asn1-types
     , base            ^>=4.14.3.0
+    , base32
     , binary
     , bytestring
     , cryptonite
     , hedgehog
+    , memory
     , tahoe-chk
     , tahoe-ssk
     , tasty
     , tasty-hedgehog
     , tasty-hunit
+    , text
     , x509
 
 -- A helper for generating RSA key pairs for use by the test suite.
@@ -143,4 +149,5 @@ executable make-keypairs
     , base
     , bytestring
     , cryptonite
+    , tahoe-ssk
     , x509
diff --git a/test/Generators.hs b/test/Generators.hs
index be8e2b78e313ddbaff67c27831d3837bb1fca991..21bee5bb4b2c369d5f52d99e48feecb242cecbdb 100644
--- a/test/Generators.hs
+++ b/test/Generators.hs
@@ -3,7 +3,6 @@ module Generators where
 import Crypto.Cipher.Types (makeIV)
 import Crypto.Hash (HashAlgorithm (hashDigestSize))
 import Crypto.Hash.Algorithms (SHA256 (SHA256))
-import qualified Crypto.PubKey.RSA as RSA
 import Data.ASN1.BinaryEncoding (DER (DER))
 import Data.ASN1.Encoding (ASN1Decoding (decodeASN1), ASN1Encoding (encodeASN1))
 import Data.ASN1.Types (ASN1Object (fromASN1, toASN1))
@@ -17,7 +16,8 @@ import qualified Hedgehog.Gen as Gen
 import qualified Hedgehog.Range as Range
 import Tahoe.CHK.Merkle (MerkleTree (..), makeTreePartial)
 import Tahoe.SDMF (Share (..))
-import Tahoe.SDMF.Internal.Share (HashChain (HashChain), SDMF_IV (..))
+import Tahoe.SDMF.Internal.Share (HashChain (HashChain))
+import qualified Tahoe.SDMF.Keys as Keys
 
 rootHashLength :: Int
 rootHashLength = 32
@@ -28,10 +28,6 @@ ivLength = 16
 signatureLength :: Range.Range Int
 signatureLength = Range.linear 250 260
 
-newtype KeyPair = KeyPair RSA.PrivateKey
-toPrivateKey (KeyPair privKey) = privKey
-toPublicKey = RSA.private_pub . toPrivateKey
-
 {- | Generate SDMF shares.  The contents of the share are not necessarily
  semantically valid.
 -}
@@ -45,17 +41,17 @@ shares = do
             Share
                 <$> Gen.word64 Range.exponentialBounded -- shareSequenceNumber
                 <*> Gen.bytes (Range.singleton rootHashLength) -- shareRootHash
-                <*> pure (SDMF_IV iv') -- shareIV
+                <*> pure (Keys.SDMF_IV iv') -- shareIV
                 <*> Gen.word8 Range.exponentialBounded -- shareTotalShares
                 <*> Gen.word8 Range.exponentialBounded -- shareRequiredShares
                 <*> Gen.word64 Range.exponentialBounded -- shareSegmentSize
                 <*> Gen.word64 Range.exponentialBounded -- shareDataLength
-                <*> pure (toPublicKey keypair) -- shareVerificationKey
+                <*> pure (Keys.toPublicKey keypair) -- shareVerificationKey
                 <*> Gen.bytes signatureLength -- shareSignature
                 <*> shareHashChains -- shareHashChain
                 <*> merkleTrees (Range.singleton 1) -- shareBlockHashTree
                 <*> (LB.fromStrict <$> Gen.bytes (Range.exponential 0 1024)) -- shareData
-                <*> (pure . LB.toStrict . toDER . PrivKeyRSA . toPrivateKey) keypair -- shareEncryptedPrivateKey
+                <*> (pure . LB.toStrict . toDER . PrivKeyRSA . Keys.toPrivateKey) keypair -- shareEncryptedPrivateKey
   where
     toDER = encodeASN1 DER . flip toASN1 []
 
@@ -67,7 +63,7 @@ shares = do
  challenging, this implementation just knows a few RSA key pairs already and
  will give back one of them.
 -}
-genRSAKeys :: MonadGen m => m KeyPair
+genRSAKeys :: MonadGen m => m Keys.KeyPair
 genRSAKeys = Gen.element (map rsaKeyPair rsaKeyPairBytes)
 
 -- I'm not sure how to do IO in MonadGen so do the IO up front unsafely (but
@@ -76,13 +72,13 @@ rsaKeyPairBytes :: [LB.ByteString]
 {-# NOINLINE rsaKeyPairBytes #-}
 rsaKeyPairBytes = unsafePerformIO $ mapM (\n -> LB.readFile ("test/data/rsa-privkey-" <> show n <> ".der")) [0 .. 4 :: Int]
 
-rsaKeyPair :: LB.ByteString -> KeyPair
+rsaKeyPair :: LB.ByteString -> Keys.KeyPair
 rsaKeyPair bs = do
     let (Right kp) = do
             asn1s <- first show (decodeASN1 DER bs)
             (r, _) <- fromASN1 asn1s
             case r of
-                PrivKeyRSA pk -> pure $ KeyPair pk
+                PrivKeyRSA pk -> pure $ Keys.KeyPair pk
                 _ -> error "Expected RSA Private Key"
     kp
 
diff --git a/test/Spec.hs b/test/Spec.hs
index 98914dd558e645bf065f40a11f6e8d49e9212e4d..e185dc49dcb1d803fa8f0b21ec09d97bc8fe549d 100644
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -6,16 +6,29 @@ import Hedgehog (
     tripping,
  )
 
+import Crypto.Cipher.Types (makeIV)
+import Data.ASN1.BinaryEncoding (DER (DER))
+import Data.ASN1.Encoding (decodeASN1')
 import qualified Data.Binary as Binary
 import Data.Binary.Get (ByteOffset)
+import qualified Data.ByteArray as ByteArray
+import qualified Data.ByteString as B
+import Data.ByteString.Base32 (encodeBase32Unpadded)
 import qualified Data.ByteString.Lazy as LB
-import Generators (shareHashChains, shares)
+import qualified Data.Text as T
+import Generators (genRSAKeys, shareHashChains, shares)
 import System.IO (hSetEncoding, stderr, stdout, utf8)
 import Tahoe.SDMF (Share)
+import Tahoe.SDMF.Internal.Keys (signatureKeyFromBytes, signatureKeyToBytes)
+import qualified Tahoe.SDMF.Keys as Keys
 import Test.Tasty (TestTree, defaultMain, testGroup)
 import Test.Tasty.HUnit (assertEqual, testCase)
 import Test.Tasty.Hedgehog (testProperty)
 
+-- The test suite compares against some hard-coded opaque strings.  These
+-- expected values were determined using the expected_values.py program in
+-- this directory.
+
 tests :: TestTree
 tests =
     testGroup
@@ -24,6 +37,102 @@ tests =
             property $ do
                 hashChain <- forAll shareHashChains
                 tripping hashChain Binary.encode decode'
+        , testProperty "Signatures round-trip through signatureKeyToBytes . signatureKeyFromBytes" $
+            property $ do
+                key <- forAll genRSAKeys
+                tripping (Keys.Signature . Keys.toPrivateKey $ key) signatureKeyToBytes signatureKeyFromBytes
+        , testCase "Signature byte-serializations round-trip through signatureKeyFromBytes . signatureKeyToBytes" $ do
+            let keyPaths =
+                    [ -- Check ours
+                      "test/data/rsa-privkey-0.der"
+                    , "test/data/rsa-privkey-1.der"
+                    , "test/data/rsa-privkey-2.der"
+                    , "test/data/rsa-privkey-3.der"
+                    , "test/data/rsa-privkey-4.der"
+                    , -- And one from Tahoe-LAFS
+                      "test/data/tahoe-lafs-generated-rsa-privkey.der"
+                    ]
+                checkSignatureRoundTrip p =
+                    B.readFile p >>= \original ->
+                        let (Right sigKey) = signatureKeyFromBytes original
+                            serialized = signatureKeyToBytes sigKey
+                         in do
+                                -- They should decode to the same structure.  This
+                                -- has the advantage of representing differences a
+                                -- little more transparently than the next
+                                -- assertion.
+                                assertEqual
+                                    "decodeASN1 original /= decodeASN1 serialized"
+                                    (decodeASN1' DER original)
+                                    (decodeASN1' DER serialized)
+                                -- Also check the raw bytes in case there
+                                -- are different representations of the
+                                -- structure possible.  The raw bytes
+                                -- matter because we hash them in key
+                                -- derivations.
+                                assertEqual "original /= serialized" original serialized
+            -- Check them all
+            mapM_ checkSignatureRoundTrip keyPaths
+        , testCase "derived keys equal known-correct values" $
+            -- The path is relative to the root of the package, which is where
+            -- at least some test runners will run the test process.  If
+            B.readFile "test/data/rsa-privkey-0.der" >>= \privBytes ->
+                let -- Load the test key.
+                    (Right sigKey) = signatureKeyFromBytes privBytes
+
+                    -- Hard-code the expected result.
+                    expectedWriteKey = ("v7iymuxkc5yv2fomi3xwbjdd4e" :: T.Text)
+                    expectedReadKey = ("6ir6husgx6ubro3tbimmzskqri" :: T.Text)
+                    expectedDataKey = ("bbj67exlrkfcaqutwlgwvukbfe" :: T.Text)
+                    expectedStorageIndex = ("cmkuloz2t6fhsh7npxxteba6sq" :: T.Text)
+                    expectedWriteEnablerMaster = ("qgptod5dsanfep2kbimvxl2yixndnoks7ndoeamczj7g33gokcvq" :: T.Text)
+                    expectedWriteEnabler = ("bg4ldrgfyiffufltcuttr3cnrmrjfpoxc65qdoqa6d5izkzofl5q" :: T.Text)
+
+                    -- Constants involved in the derivation.  These agree with
+                    -- those used to generate the above expected values.
+                    (Just iv) = Keys.SDMF_IV <$> makeIV (B.replicate 16 0x42)
+                    peerid = B.replicate 20 0x42
+
+                    -- Derive all the keys.
+                    (Just w@(Keys.Write _ derivedWriteKey)) = Keys.deriveWriteKey sigKey
+                    (Just r@(Keys.Read _ derivedReadKey)) = Keys.deriveReadKey w
+                    (Just (Keys.Data _ derivedDataKey)) = Keys.deriveDataKey iv r
+                    (Keys.StorageIndex derivedStorageIndex) = Keys.deriveStorageIndex r
+                    wem@(Keys.WriteEnablerMaster derivedWriteEnablerMaster) = Keys.deriveWriteEnablerMaster w
+                    (Keys.WriteEnabler derivedWriteEnabler) = Keys.deriveWriteEnabler peerid wem
+                    -- A helper to format a key as text for convenient
+                    -- comparison to expected value.
+                    fmtKey = T.toLower . encodeBase32Unpadded . ByteArray.convert
+                 in do
+                        -- In general it might make more sense to convert expected
+                        -- into ScrubbedBytes instead of converting derived into
+                        -- ByteString but ScrubbedBytes doesn't have a useful Show
+                        -- instance so we go the other way.  We're not worried about
+                        -- the safety of these test-only keys anyway.
+                        assertEqual
+                            "writekey: expected /= derived"
+                            expectedWriteKey
+                            (fmtKey derivedWriteKey)
+                        assertEqual
+                            "readkey: expected /= derived"
+                            expectedReadKey
+                            (fmtKey derivedReadKey)
+                        assertEqual
+                            "datakey: expected /= derived"
+                            expectedDataKey
+                            (fmtKey derivedDataKey)
+                        assertEqual
+                            "storage index: expected /= derived"
+                            expectedStorageIndex
+                            (T.toLower . encodeBase32Unpadded $ derivedStorageIndex)
+                        assertEqual
+                            "write enabler master: expected /= derived"
+                            expectedWriteEnablerMaster
+                            (fmtKey derivedWriteEnablerMaster)
+                        assertEqual
+                            "write enabler: expected /= derived"
+                            expectedWriteEnabler
+                            (fmtKey derivedWriteEnabler)
         , testProperty "Share round-trips through bytes" $
             property $ do
                 share <- forAll shares
diff --git a/test/data/rsa-privkey-0.der b/test/data/rsa-privkey-0.der
index ad64c6304d32d06ba74f9a9234de2cd14a6820ab..e3bb393ed5637f17db469248532b44420cfefc8f 100644
Binary files a/test/data/rsa-privkey-0.der and b/test/data/rsa-privkey-0.der differ
diff --git a/test/data/rsa-privkey-1.der b/test/data/rsa-privkey-1.der
index ad64c6304d32d06ba74f9a9234de2cd14a6820ab..9bffed68a1b2dcae3bb7a666d71540b26fbfd8d8 100644
Binary files a/test/data/rsa-privkey-1.der and b/test/data/rsa-privkey-1.der differ
diff --git a/test/data/rsa-privkey-2.der b/test/data/rsa-privkey-2.der
index ad64c6304d32d06ba74f9a9234de2cd14a6820ab..df86447a4ead9bde2e5000614e59cd2149ae505e 100644
Binary files a/test/data/rsa-privkey-2.der and b/test/data/rsa-privkey-2.der differ
diff --git a/test/data/rsa-privkey-3.der b/test/data/rsa-privkey-3.der
index ad64c6304d32d06ba74f9a9234de2cd14a6820ab..b8a87219665ec0f04855d05596603032725e0d36 100644
Binary files a/test/data/rsa-privkey-3.der and b/test/data/rsa-privkey-3.der differ
diff --git a/test/data/rsa-privkey-4.der b/test/data/rsa-privkey-4.der
index ad64c6304d32d06ba74f9a9234de2cd14a6820ab..c9d0474ce9b4f22a4f32220cad76659ae54b0b98 100644
Binary files a/test/data/rsa-privkey-4.der and b/test/data/rsa-privkey-4.der differ
diff --git a/test/data/tahoe-lafs-generated-rsa-privkey.der b/test/data/tahoe-lafs-generated-rsa-privkey.der
new file mode 100644
index 0000000000000000000000000000000000000000..ba71aa1212413a9f581dbc52594ea9fee5338ff0
Binary files /dev/null and b/test/data/tahoe-lafs-generated-rsa-privkey.der differ
diff --git a/test/expected_values.py b/test/expected_values.py
new file mode 100644
index 0000000000000000000000000000000000000000..31d18d9e3a05396116d406689947cfee020a7238
--- /dev/null
+++ b/test/expected_values.py
@@ -0,0 +1,37 @@
+# Tested on Python 3.9.15 against Tahoe-LAFS bc79cf0a11f06bbdc02a5bb41c6f41fcff727ea5
+#
+
+from allmydata.crypto import rsa
+from allmydata.mutable.common import derive_mutable_keys
+from allmydata.util import base32
+from allmydata.util import hashutil
+
+# Arbitrarily select an IV.
+iv = b"\x42" * 16
+# And "peer id"
+peerid = b"\x42" * 20
+
+with open("data/rsa-privkey-0.der", "rb") as f:
+    (priv, pub) = rsa.create_signing_keypair_from_string(f.read())
+
+writekey, encprivkey, fingerprint = derive_mutable_keys((pub, priv))
+readkey = hashutil.ssk_readkey_hash(writekey)
+datakey = hashutil.ssk_readkey_data_hash(iv, readkey)
+storage_index = hashutil.ssk_storage_index_hash(readkey)
+write_enabler_master = hashutil.ssk_write_enabler_master_hash(writekey)
+write_enabler = hashutil.ssk_write_enabler_hash(writekey, peerid)
+
+print("SDMF")
+print("writekey: ", base32.b2a(writekey))
+print("readkey: ", base32.b2a(readkey))
+print("datakey (iv = \x42 * 16): ", base32.b2a(datakey))
+print("storage index: ", base32.b2a(storage_index))
+print("encrypted private key: ", base32.b2a(encprivkey))
+print("signature key hash: ", base32.b2a(fingerprint))
+print("write enabler master: ", base32.b2a(write_enabler_master))
+print("write enabler (peerid = \x42 * 20): ", base32.b2a(write_enabler))
+
+(priv, pub) = rsa.create_signing_keypair(2048)
+priv_bytes = rsa.der_string_from_signing_key(priv)
+with open("data/tahoe-lafs-generated-rsa-privkey.der", "wb") as f:
+    f.write(priv_bytes)