diff --git a/cabal.project b/cabal.project index 55be17a13a683db9dd1f7743b243c5f56a0f587a..80fc512d3f3319213956e32b7fef370e50b09ec5 100644 --- a/cabal.project +++ b/cabal.project @@ -2,6 +2,7 @@ packages: . -- These aren't released on hackage yet so we have to grab them -- straight from the vcs host. Probably should do some releases -- soon. + ../tahoe-ssk https://whetstone.private.storage/privatestorage/tahoe-chk/-/archive/0.1.0.1/tahoe-chk-0.1.0.1.tar.gz https://whetstone.private.storage/privatestorage/tahoe-great-black-swamp/-/archive/0.2.0.2/tahoe-great-black-swamp-0.2.0.2.tar.gz diff --git a/gbs-downloader.cabal b/gbs-downloader.cabal index ba2597aa1d6bdb02aadeb79eb14fd2a3f267a66b..af2f068d8bb5584e08c77673d60a82ea56fa2254 100644 --- a/gbs-downloader.cabal +++ b/gbs-downloader.cabal @@ -109,6 +109,7 @@ library , servant-client-core , tahoe-chk , tahoe-great-black-swamp >=0.2 && <0.3 + , tahoe-ssk , text , yaml @@ -174,18 +175,23 @@ test-suite gbs-downloader-test -- Test dependencies. build-depends: + , asn1-encoding + , asn1-types , base , base32 , binary , bytestring , containers , crypto-api + , cryptonite , data-default-class , gbs-downloader , hedgehog , tahoe-chk + , tahoe-ssk , tasty , tasty-hedgehog , tasty-hunit , text + , x509 , yaml diff --git a/test/Generators.hs b/test/Generators.hs index 3adc7c662bbccce84fecc99c0cb6251bac5eb1fa..7827221f4e4200997a4ff55165e44577bdcb0af1 100644 --- a/test/Generators.hs +++ b/test/Generators.hs @@ -1,11 +1,19 @@ module Generators where +import qualified Crypto.PubKey.RSA as RSA +import Data.ASN1.BinaryEncoding (DER (DER)) +import Data.ASN1.Encoding (ASN1Decoding (decodeASN1)) +import Data.ASN1.Types (ASN1Object (fromASN1)) +import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Base32 (encodeBase32Unpadded) +import qualified Data.ByteString.Lazy as LB import Data.Int (Int64) import qualified Data.Text as T +import Data.X509 (PrivKey (PrivKeyRSA)) import Hedgehog (MonadGen) import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range +import System.IO.Unsafe (unsafePerformIO) import Tahoe.Announcement (Announcements (..), StorageServerAnnouncement (..)) import Tahoe.CHK.Types (Parameters (..)) @@ -37,3 +45,30 @@ genStorageServerAnnouncements = <$> Gen.maybe (Gen.text (Range.linear 16 32) Gen.ascii) <*> Gen.maybe (Gen.text (Range.linear 16 32) Gen.ascii) <*> Gen.maybe (Gen.bytes $ Range.singleton 32) + +{- | Build RSA key pairs. + + Because the specific bits of the key pair shouldn't make any difference to + any application logic, generating new RSA key pairs is expensive, and + generating new RSA key pairs in a way that makes sense in Hedgehog is + challenging, this implementation just knows a few RSA key pairs already and + will give back one of them. +-} +genRSAKeys :: MonadGen m => m RSA.PrivateKey +genRSAKeys = Gen.element (map rsaKeyPair rsaKeyPairBytes) + +-- I'm not sure how to do IO in MonadGen so do the IO up front unsafely (but +-- hopefully not really unsafely). +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.PrivateKey +rsaKeyPair bs = do + let (Right kp) = do + asn1s <- first show (decodeASN1 DER bs) + (r, _) <- fromASN1 asn1s + case r of + PrivKeyRSA pk -> pure pk + _ -> error "Expected RSA Private Key" + kp diff --git a/test/Spec.hs b/test/Spec.hs index 91a9191fd9cb0223b4fb905c9838bf9d612c93df..69fffcd68e29ef8b612a464257afc9a33b53d86e 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -3,6 +3,7 @@ module Main where import Control.Exception (Exception, throwIO) import Control.Monad (replicateM, zipWithM_) import Control.Monad.IO.Class (liftIO) +import Crypto.Cipher.Types (nullIV) import Crypto.Classes (buildKey) import qualified Data.Binary as Binary import qualified Data.ByteString as B @@ -15,7 +16,7 @@ import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Data.Word (Word16) import qualified Data.Yaml as Yaml -import Generators (genAnnouncements, genParameters) +import Generators (genAnnouncements, genParameters, genRSAKeys) import Hedgehog (MonadGen, diff, forAll, property, tripping) import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range @@ -41,6 +42,8 @@ import Tahoe.Download ( LookupServer, download, ) +import qualified Tahoe.SDMF as SDMF +import qualified Tahoe.SDMF.Keys as SDMF.Keys import Tahoe.Server (memoryStorageServer) import Test.Tasty (TestTree, defaultMain, testGroup) import Test.Tasty.HUnit (assertEqual, testCase) @@ -244,7 +247,7 @@ tests = "download should fail with details about unreachable server" (Left (NotEnoughDecodedShares{notEnoughDecodedSharesNeeded = 3, notEnoughDecodedSharesFound = 0})) result - , testProperty "success" $ + , testProperty "chk success" $ property $ do -- If we can recover enough distinct, decodeable shares from the -- configured servers then we can recover the application data. @@ -291,6 +294,51 @@ tests = -- Recover the plaintext from the servers. result <- liftIO $ download serverAnnouncements cap lookupServer diff (Right plaintext) (==) result + , testProperty "ssk success" $ + property $ do + -- Like "chk success" above, but for SDMF (a case of SSK). + plaintext <- forAll $ BL.fromStrict <$> Gen.bytes (Range.exponential 56 1024) + sequenceNumber <- forAll $ Gen.integral (Range.exponential 1 10000) + keypair <- SDMF.Keys.KeyPair <$> forAll genRSAKeys + Parameters{paramRequiredShares = required, paramTotalShares = total} <- forAll genParameters + + -- Since multiple shares can be placed on a single server, as long + -- as we have one server we have a valid case. Since some shares + -- might be placed non-optimally it is also nice to allow for some + -- empty servers so allow for that as well. + let numServers = Range.linear 1 (fromIntegral total + 1) + serverIDs = Gen.text (Range.singleton 2) Gen.ascii + serverIDs' <- forAll $ Gen.set numServers serverIDs + + perServerShareCount <- + forAll $ + genListWithSum (length serverIDs') (fromIntegral total) + + -- Make the servers. + servers <- liftIO $ replicateM (length serverIDs') memoryStorageServer + + -- Derive the keys, encode the data. + let -- Not a very good IV choice in reality but it's okay for + -- this test where confidentiality and key secrecy is not + -- particularly a concern. + iv = SDMF.Keys.SDMF_IV nullIV + signatureKey = SDMF.Keys.toSignatureKey keypair + (Just writeKey) = SDMF.Keys.deriveWriteKey signatureKey + (Just readKey) = SDMF.Keys.deriveReadKey writeKey + (Just dataKey) = SDMF.Keys.deriveDataKey iv readKey + ciphertext = SDMF.encrypt dataKey plaintext + (shares, writeCap) <- liftIO $ SDMF.encode keypair sequenceNumber required total ciphertext + let readCap = SDMF.writerReader writeCap + -- Distribute the shares. + liftIO $ placeMutable writeCap (Binary.encode <$> shares) perServerShareCount servers 0 + + let serverMap = Map.fromList $ zip (Set.toList serverIDs') servers + lookupServer = someServers serverMap + serverAnnouncements = Map.fromSet makeAnn serverIDs' + + -- Recover the plaintext from the servers. + result <- liftIO $ download serverAnnouncements readCap lookupServer + diff (Right plaintext) (==) result , testCase "immutable upload/download to using Great Black Swamp" $ do pure () -- Consider moving these tests to another module, they're pretty @@ -323,6 +371,9 @@ tests = -- Exactly match the nonsense makeAnn spits out parseURL = T.take 2 . T.drop 5 + placeMutable :: SDMF.Writer -> [BL.ByteString] -> [Int] -> [StorageServer] -> Int -> IO () + placeMutable = undefined + --- PHILOSOFY -- We wish that share numbers were an opaque type instead of a -- numeric/integral type. This is not the place to argue the point