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