From 92e46194f340164ee1a24b05bb47e01b7fbe25e7 Mon Sep 17 00:00:00 2001
From: Jean-Paul Calderone <exarkun@twistedmatrix.com>
Date: Fri, 12 May 2023 14:31:03 -0400
Subject: [PATCH] Fix key derivations through "datakey"

* Add direct tests for signature key serialization/deserialization
* Fix the RSA Private Key serializer to use PKCS8 like Tahoe-LAFS expects.
* Fix the key generator to emit keys in this format.
* Fix the key generator to generate 2048 bit keys.
* Fix the derivation functions to make the key values internally consistent.
---
 make-keypairs/Main.hs                         |  18 +++--
 src/Tahoe/SDMF/Internal/Keys.hs               |  72 +++++++++++++++---
 tahoe-ssk.cabal                               |   1 +
 test/Spec.hs                                  |  62 ++++++++++++---
 .../data/tahoe-lafs-generated-rsa-privkey.der | Bin 0 -> 1219 bytes
 5 files changed, 123 insertions(+), 30 deletions(-)
 create mode 100644 test/data/tahoe-lafs-generated-rsa-privkey.der

diff --git a/make-keypairs/Main.hs b/make-keypairs/Main.hs
index d77171e..3e5eea0 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
index 40ab740..3478225 100644
--- a/src/Tahoe/SDMF/Internal/Keys.hs
+++ b/src/Tahoe/SDMF/Internal/Keys.hs
@@ -3,14 +3,16 @@ 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))
-import Data.ASN1.Types (ASN1Object (toASN1))
+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)
@@ -20,14 +22,19 @@ import Data.X509 (PrivKey (PrivKeyRSA), PubKey (PubKeyRSA))
 import Tahoe.CHK.Crypto (taggedHash, taggedPairHash)
 import Tahoe.CHK.Server (StorageServerID)
 
-newtype KeyPair = KeyPair {toPrivateKey :: RSA.PrivateKey}
+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}
 
@@ -74,10 +81,10 @@ 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 sbs
+    Read <$> key <*> pure (ByteArray.convert sbs)
   where
-    sbs = writeKeyBytes w
-    key = maybeCryptoError . cipherInit . taggedHash keyLength mutableReadKeyTag . ByteArray.convert $ sbs
+    sbs = taggedHash keyLength mutableReadKeyTag . ByteArray.convert . writeKeyBytes $ w
+    key = maybeCryptoError . cipherInit $ sbs
 
 mutableReadKeyTag :: B.ByteString
 mutableReadKeyTag = "allmydata_mutable_writekey_to_readkey_v1"
@@ -85,10 +92,10 @@ 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 sbs
+    Data <$> key <*> pure (ByteArray.convert sbs)
   where
-    sbs = readKeyBytes r
-    key = maybeCryptoError . cipherInit . taggedPairHash keyLength mutableDataKeyTag (B.pack . ByteArray.unpack $ iv) . ByteArray.convert $ sbs
+    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"
@@ -108,4 +115,49 @@ verificationKeyToBytes = LB.toStrict . encodeASN1 DER . flip toASN1 [] . PubKeyR
  PublicKey.
 -}
 signatureKeyToBytes :: Signature -> B.ByteString
-signatureKeyToBytes = LB.toStrict . encodeASN1 DER . flip toASN1 [] . PrivKeyRSA . unSignature
+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/tahoe-ssk.cabal b/tahoe-ssk.cabal
index 2b45eec..d7e3e7b 100644
--- a/tahoe-ssk.cabal
+++ b/tahoe-ssk.cabal
@@ -149,4 +149,5 @@ executable make-keypairs
     , base
     , bytestring
     , cryptonite
+    , tahoe-ssk
     , x509
diff --git a/test/Spec.hs b/test/Spec.hs
index 4031d0c..b576ac0 100644
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -9,7 +9,6 @@ import Hedgehog (
 import Crypto.Cipher.Types (makeIV)
 import Data.ASN1.BinaryEncoding (DER (DER))
 import Data.ASN1.Encoding (decodeASN1')
-import Data.ASN1.Types (ASN1Object (fromASN1))
 import qualified Data.Binary as Binary
 import Data.Binary.Get (ByteOffset)
 import qualified Data.ByteArray as ByteArray
@@ -17,10 +16,10 @@ 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))
-import Generators (shareHashChains, shares)
+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)
@@ -38,23 +37,62 @@ 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 (Just iv) = Keys.SDMF_IV <$> makeIV (B.replicate 16 0x42)
-                    expectedWriteKey = ("ae6e6cgcllhty4z5l4dp5v7gee" :: T.Text)
-                    expectedReadKey = ("rbx5xh5rztefvazy7sq32sw34y" :: T.Text)
-                    expectedDataKey = ("4ay4y6itvk7cvynpyok3qmxf5y" :: T.Text)
+                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)
 
-                    (Right asn1s) = decodeASN1' DER privBytes
-                    (Right (PrivKeyRSA privKey, [])) = fromASN1 asn1s
-                    (Just w@(Keys.Write _ derivedWriteKey)) = Keys.deriveWriteKey (Keys.Signature privKey)
+                    -- Derive all the keys.
+                    (Just iv) = Keys.SDMF_IV <$> makeIV (B.replicate 16 0x42)
+                    (Just w@(Keys.Write _ derivedWriteKey)) = Keys.deriveWriteKey sigKey
                     (Just r@(Keys.Read _ derivedReadKey)) = Keys.deriveReadKey w
                     (Just d@(Keys.Data _ derivedDataKey)) = Keys.deriveDataKey iv r
 
-                    -- Format a key as text for convenient comparison to
-                    -- expected value.
+                    -- 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
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
GIT binary patch
literal 1219
zcmV;!1U&mNf&{+;0RS)!1_>&LNQU<f0RaI800e>rsW5^Br2+u}0)hbn0MkVx)W^`s
zT=ceR<@c+61=VZoh7in0{a?dfh>8lfC!2+YA^WG?@a0j$$&j{td$?FsJyuDZ^ypBS
z_d<`fA)vTFbsh+LTilnwx0wJvc1}B`L=w{6Rx*{Xvr<GN0%q2VrW$dsJofm#J+LPi
z=ocf-Z<VS5@QO_l;$fYR4M9s)rQ`iB+hoC74CvHlHwqbu?G<kUjUhB<vF)Vf3Cye$
zbq`iPxN!BwY_d%Cg^~T~?JOw!3r;Fb%&ff*@hz9^1IB5{F%6X}v9)IEJJ9gvIijp@
zw;+)%?<BY-%5~GMp>M<UAzuI&k*)zRp^I_3>&-ya)ve++DAXlae*yym009Dm0RU6q
zq@GyacuoHh)4)i58TGF2^GEzIJ$*-^W#*{%>#g~|9pLBbl6y#R7bmiP-?S7_Y|s@8
zkPyI<4#-OKh7P--ssFO}rblDq63DK@JEK+?v)a6-UAGd+{dKK*Lk~3wgKokc(~*2~
z@17^>y+HK7&|BC{@1~C-U9v<Hs;j%kZ|uHVVw~NqHm8<*tG9r=8NPT2j!llFj(Z6N
zDt*;hXL*GYcdo;;l@CqWzEA;QRTA`iQg@|~(}ZUK67wwGNc{Kv(FEVR8E)J4`$oz2
zQ0t6VCak_oaLXi>bHpr6d*#(6uSA{>%#xtG;)=9^-p?2%1Knf>b|}}->`?-NfdJ!T
zD76`H;k1RCH`QVqOpyiZGlRb#vvFRYquXVU>=rY%V%h6aT0e0NrSD}5ml{1(K`+hv
zrzekeapehlml&gPoYb+;s$BpO{60MLHLHPB7MC7Rd#OJ>{v7;mr`38Zy*J?EZCmaA
zA3yql=n$%0fO$W5{IPC~q+yh@l+^-(fdK8>)*DM>6vZ0t6b#x3y7y-(saR!WCvD1&
z!mmOElhcpw#BpGciiG4sIC%iSfd<vH&lfbCWw->zj80JL)ep03ObBNED@`i$F^vTI
z9BY&l+Sl5I^F0)9z6f;w9hezdJPySsP_JHGsKbnz<AD78OKhJ8p|sQJ<>fsGwF3fy
zfdJio8YTW0D~3w7#5qWeQIy(j9j)<~7&s7c`!AIzxb%;G+CpcfAp*BP7|gC=xOzy>
zFNi;0FF>o#L%UR!Y6l1XgXZ%kQ_rtv+%8M=t1sG<a_7|$n`Uc4AWmQ0_@zsd8;H2L
z7Rq~a8swQk!_24!-se16%^;AB()x0H+1UbtfdIQ0HoMh&q&2=yNqs77zm+FKZ~!I`
z?oGf8?cFI-tZcVK(MMG((@bEuZSYVOQj2TBIo=5MO81IcQCYC>oi*}oWKJKXcu@qr
z=@|i*X)=A0c#K_0LWBbJaOwICfPTq9%uLzppMbIYw2aK9r+g@a3H$KEsH3tY`QdFg
z2)zP<fdIIO9;3ptIu!}b=K1_W*^taDLn8&)uug3Vyg*$4CsS)}){?gplWZ9jDb70Y
zC*ChB7^Al0S4gh#=*}ZuBY5$KE=gA7&-wzzMLDnBf40dC)V_lL5@IPw0U`3eFt&>q
h0!T<a8YOmx(%~GIK`x8-11ikYSl^kF>b~%MBW9~IRyhCw

literal 0
HcmV?d00001

-- 
GitLab