diff --git a/README.md b/README.md
index b5fa4d9f90c5430e6279adce6344bb06ecd0dbc7..173dc50acc465539b4bcfc940c923d30227c690b 100644
--- a/README.md
+++ b/README.md
@@ -22,3 +22,82 @@ with the benefit of the experience gained from creating and maintaining the Pyth
 a number of implementation decisions can be made differently to produce a more efficient, more flexible, simpler implementation and API.
 Also,
 the Python implementation claims no public library API for users outside of the Tahoe-LAFS project itself.
+
+## Cryptographic Library Choice
+
+This library uses cryptonite for cryptography,
+motivated by the following considerations.
+
+SDMF uses
+* SHA256 for tagged hashes for key derivation and for integrity (XXX right word?) checks on some data.
+* AES128 for encryption of the signature key and the application plaintext data.
+* RSA for signatures proving write authority.
+
+There are a number of Haskell libraries that provide all of these:
+
+* Crypto
+  * Does not support the AES mode we require (CTR).
+
+* HsOpenSSL
+  * Bindings to a C library, OpenSSL, which may complicate the build process.
+  * OpenSSL's security and reliability track record also leaves something to be desired.
+
+* cryptonite
+  * Has all of the primitive cryptographic functionality we need.
+
+We want a library that:
+
+* Can be used with reflex-platform
+  * ghc 8.6.5 compatible
+* Can be cross-compiled to different targets from x86_64-linux
+  * Mainly armeabi and armv7
+* Is suitable for real-world security purposes
+  * not a demo or a toy library
+    * documents its limitations
+  * is well-tested
+  * avoids real-world pitfalls (side-channel attacks, etc), not just textbook issues
+  * has more than a handful of other users
+  * is well-maintained
+    * developers are responsive to security reports
+    * has a channel for security-related disclosures
+	* has sound documentation for proper, safe usage
+
+And,
+of course,
+implements the required functionality.
+
+### SHA256
+
+There are a number of Haskell libraries that provide this primitive:
+
+* Crypto
+* HsOpenSSL
+* SHA
+* cryptohash
+* cryptonite
+* dhall
+* hashing
+* nettle
+* sbv
+* tls
+
+### AES128
+
+* Crypto
+* HsOpenSSL
+* cipher-aes
+* cipher-aes128
+* crypto
+* cryptocipher
+* cryptonite
+* cryptostore
+* nettle
+
+### RSA
+
+SDMF depends on RSA for signatures proving write authority.
+
+* Crypto
+* HsOpenSSL
+* RSA
+* cryptonite
diff --git a/make-keypairs/Main.hs b/make-keypairs/Main.hs
index c537ae4d3420c2c0ee42f80ac67c4d3cd281cd2b..3e5eea01f14048b4fca3c12a2d041a2be5de5fa6 100644
--- a/make-keypairs/Main.hs
+++ b/make-keypairs/Main.hs
@@ -1,11 +1,9 @@
 module Main where
 
-import Codec.Crypto.RSA (generateKeyPair)
-import Crypto.Random (CryptoRandomGen (newGenIO), SystemRandom)
-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 qualified Crypto.PubKey.RSA as RSA
+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
@@ -17,11 +15,16 @@ count = 5
 
 main :: IO ()
 main = do
-    g <- newGenIO :: IO SystemRandom
-    mapM_ (genKey g) [0 .. count - 1]
+    mapM_ genKey [0 .. count - 1]
 
-genKey :: (Show a, CryptoRandomGen c) => c -> a -> IO ()
-genKey g n =
-    let (_, priv, _) = generateKeyPair g bits
-        bytes = encodeASN1 DER (toASN1 priv [])
-     in LB.writeFile ("test/data/rsa-privkey-" <> show n <> ".der") bytes
+genKey :: Show a => a -> IO ()
+genKey n = do
+    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
index 1365f12a6952e6f2fd37c938696324552d6bb703..e40d6fbefa909e638a689f7d802a3b7ee53bc1d4 100644
--- a/src/Tahoe/SDMF/Internal/Keys.hs
+++ b/src/Tahoe/SDMF/Internal/Keys.hs
@@ -1,43 +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 Codec.Crypto.RSA (generateKeyPair)
-import Crypto.Cipher.AES128 (AESKey128)
-import qualified Crypto.PubKey.RSA.Types as RSA
-import "crypto-api" Crypto.Random (SystemRandom, newGenIO)
+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 Tahoe.CHK.Server (StorageServerID)
+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 Pair = Pair {unPair :: RSA.KeyPair}
 newtype Verification = Verification {unVerification :: RSA.PublicKey}
 newtype Signature = Signature {unSignature :: RSA.PrivateKey}
-newtype Write = Write {unWrite :: AESKey128}
-newtype Read = Read {unRead :: AESKey128}
+    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 B.ByteString
-data WriteEnabler = WriteEnabler StorageServerID B.ByteString
+newtype WriteEnablerMaster = WriteEnablerMaster ByteArray.ScrubbedBytes
+
+newtype WriteEnabler = WriteEnabler ByteArray.ScrubbedBytes
 
-newtype Encryption = Encryption AESKey128
+data Data = Data {unData :: AES128, dataKeyBytes :: ByteArray.ScrubbedBytes}
 
--- | The size of the keys to generate.
-bits :: Int
-bits = 2048
+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 :: IO Pair
+newKeyPair :: MonadRandom m => m KeyPair
 newKeyPair = do
-    g <- newGenIO :: IO SystemRandom
-    let (_, priv, _) = generateKeyPair g bits
-    pure . Pair . RSA.KeyPair $ priv
+    (_, 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 = buildKey . taggedHash writeKeyLength mutableWriteKeyTag . signatureKeyToBytes . unSignature
+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 signature key for an SDMF share.
+-- | Compute the read key for a given write key for an SDMF share.
 deriveReadKey :: Write -> Maybe Read
-deriveReadKey = buildKey . taggedHash readKeyLength mutableReadKeyTag . encode . unWrite
+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 320730cbb4cc01ab4d036c016ee3333cddbbb9ac..e5862b476f763e1f190cceb91e649f9084bfb9b1 100644
--- a/src/Tahoe/SDMF/Internal/Share.hs
+++ b/src/Tahoe/SDMF/Internal/Share.hs
@@ -2,20 +2,22 @@
 module Tahoe.SDMF.Internal.Share where
 
 import Control.Monad (unless)
-import Crypto.Cipher.AES128 (AESKey128)
+import Crypto.Cipher.AES (AES128)
+import Crypto.Cipher.Types (IV, makeIV)
 import qualified Crypto.PubKey.RSA.Types as RSA
-import Crypto.Types (IV (IV, initializationVector))
 import Data.ASN1.BinaryEncoding (DER (DER))
 import Data.ASN1.Encoding (ASN1Encoding (encodeASN1), decodeASN1')
 import Data.ASN1.Types (ASN1Object (fromASN1, toASN1))
 import Data.Binary (Binary (..), Get, getWord8)
 import Data.Binary.Get (bytesRead, getByteString, getLazyByteString, getRemainingLazyByteString, getWord16be, getWord32be, getWord64be, isEmpty, isolate)
 import Data.Binary.Put (putByteString, putLazyByteString, putWord16be, putWord32be, putWord64be, putWord8)
+import qualified Data.ByteArray as ByteArray
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy as LB
 import Data.Word (Word16, Word64, Word8)
 import Data.X509 (PrivKey (PrivKeyRSA), PubKey (PubKeyRSA))
 import Tahoe.CHK.Merkle (MerkleTree, leafHashes)
+import Tahoe.SDMF.Internal.Keys (SDMF_IV (..))
 
 hashSize :: Int
 hashSize = 32
@@ -57,7 +59,7 @@ data Share = Share
     , -- | "R" (root of share hash merkle tree)
       shareRootHash :: B.ByteString
     , -- | The IV for encryption of share data.
-      shareIV :: IV AESKey128
+      shareIV :: SDMF_IV
     , -- | The total number of encoded shares (k).
       shareTotalShares :: Word8
     , -- | The number of shares required for decoding (N).
@@ -91,7 +93,7 @@ instance Binary Share where
         putWord8 0
         putWord64be shareSequenceNumber
         putByteString shareRootHash
-        putByteString . initializationVector $ shareIV
+        putByteString . ByteArray.convert $ shareIV
         putWord8 shareRequiredShares
         putWord8 shareTotalShares
         putWord64be shareSegmentSize
@@ -125,7 +127,12 @@ instance Binary Share where
         unless (version == 0) (fail $ "Only version 0 is supported; got version " <> show version)
         shareSequenceNumber <- getWord64be
         shareRootHash <- getByteString 32
-        shareIV <- IV <$> getByteString 16
+        ivBytes <- getByteString 16
+        shareIV <-
+            SDMF_IV <$> case makeIV ivBytes of
+                Nothing -> fail "Could not decode IV"
+                Just iv -> pure iv
+
         shareRequiredShares <- getWord8
         shareTotalShares <- getWord8
         shareSegmentSize <- getWord64be
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 b2f8b5b6fb9fb6f692e8f6429690dc42dc15296d..ed50a7cf520f56e797d624c81b281b09807c3531 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
@@ -67,19 +68,18 @@ library
     Tahoe.SDMF.Internal.Encoding
     Tahoe.SDMF.Internal.Keys
     Tahoe.SDMF.Internal.Share
+    Tahoe.SDMF.Keys
 
   build-depends:
     , asn1-encoding
     , asn1-types
     , base
+    , base32
     , binary
     , bytestring
     , cereal
-    , cipher-aes128
-    , crypto-api
-    , crypto-pubkey-types
     , cryptonite
-    , RSA
+    , memory
     , text
     , x509
 
@@ -127,19 +127,19 @@ test-suite tahoe-ssk-test
   build-depends:
     , asn1-encoding
     , asn1-types
-    , base                 ^>=4.14.3.0
+    , base            ^>=4.14.3.0
+    , base32
     , binary
     , bytestring
-    , crypto-api
-    , crypto-pubkey-types
     , cryptonite
     , hedgehog
-    , RSA
+    , 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.
@@ -153,6 +153,6 @@ executable make-keypairs
     , asn1-types
     , base
     , bytestring
-    , crypto-api
-    , crypto-pubkey-types
-    , RSA
+    , cryptonite
+    , tahoe-ssk
+    , x509
diff --git a/test/Generators.hs b/test/Generators.hs
index 52204a8096ace1950a63fe4093a6353d1d9a7d36..ba0fb89bac900c7a51a9cddc1470dc9ff0983c0c 100644
--- a/test/Generators.hs
+++ b/test/Generators.hs
@@ -1,9 +1,8 @@
 module Generators where
 
+import Crypto.Cipher.Types (makeIV)
 import Crypto.Hash (HashAlgorithm (hashDigestSize))
 import Crypto.Hash.Algorithms (SHA256 (SHA256))
-import qualified Crypto.PubKey.RSA.Types as RSA
-import Crypto.Types (IV (..))
 import Data.ASN1.BinaryEncoding (DER (DER))
 import Data.ASN1.Encoding (ASN1Decoding (decodeASN1), ASN1Encoding (encodeASN1))
 import Data.ASN1.Types (ASN1Object (fromASN1, toASN1))
@@ -19,6 +18,7 @@ import qualified Hedgehog.Range as Range
 import Tahoe.CHK.Merkle (MerkleTree (..), makeTreePartial)
 import Tahoe.SDMF (Share (..))
 import Tahoe.SDMF.Internal.Share (HashChain (HashChain))
+import qualified Tahoe.SDMF.Keys as Keys
 
 rootHashLength :: Int
 rootHashLength = 32
@@ -33,22 +33,26 @@ signatureLength = Range.linear 250 260
  semantically valid.
 -}
 shares :: MonadGen m => m Share
-shares =
-    genRSAKeys >>= \keypair ->
-        Share
-            <$> Gen.word64 Range.exponentialBounded -- shareSequenceNumber
-            <*> Gen.bytes (Range.singleton rootHashLength) -- shareRootHash
-            <*> (IV <$> Gen.bytes (Range.singleton ivLength)) -- shareIV
-            <*> Gen.word8 Range.exponentialBounded -- shareTotalShares
-            <*> Gen.word8 Range.exponentialBounded -- shareRequiredShares
-            <*> Gen.word64 Range.exponentialBounded -- shareSegmentSize
-            <*> Gen.word64 Range.exponentialBounded -- shareDataLength
-            <*> pure (RSA.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 . RSA.toPrivateKey) keypair -- shareEncryptedPrivateKey
+shares = do
+    keypair <- genRSAKeys
+    iv <- makeIV <$> Gen.bytes (Range.singleton ivLength)
+    case iv of
+        Nothing -> error "Could not build IV for SDMF share"
+        Just iv' ->
+            Share
+                <$> Gen.word64 Range.exponentialBounded -- shareSequenceNumber
+                <*> Gen.bytes (Range.singleton rootHashLength) -- shareRootHash
+                <*> 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 (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 . Keys.toPrivateKey) keypair -- shareEncryptedPrivateKey
   where
     toDER = encodeASN1 DER . flip toASN1 []
 
@@ -60,7 +64,7 @@ shares =
  challenging, this implementation just knows a few RSA key pairs already and
  will give back one of them.
 -}
-genRSAKeys :: MonadGen m => m RSA.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
@@ -69,13 +73,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 -> RSA.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 $ RSA.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 637fae75b88b5242be45f7c11d545d61e5d23f01..4ae6646483d6669aa67152b97161606db031e110 100644
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -11,18 +11,31 @@ 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 qualified Data.Text as T
 import Generators (encodingParameters, genRSAKeys, shareHashChains, shares)
 import qualified Hedgehog.Gen as Gen
 import qualified Hedgehog.Range as Range
 import System.IO (hSetEncoding, stderr, stdout, utf8)
 import qualified Tahoe.SDMF
+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
@@ -31,6 +44,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
@@ -50,9 +159,9 @@ tests =
 
                 recovered <- Tahoe.SDMF.decode writerReader (zip [0 ..] shares')
                 diff ciphertext (==) recovered
-        , testProperty "Plaintext round-trips through encrypt . decrypt" $
-            property $
-                do
+                -- , testProperty "Plaintext round-trips through encrypt . decrypt" $
+                --     property $
+                --         do
         ]
 
 {- | Load a known-correct SDMF bucket and assert that bytes in the slot it
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)