From 3d59d0f150c0ba30d7fca8a9ff82aa7c6da29e19 Mon Sep 17 00:00:00 2001 From: Jean-Paul Calderone <exarkun@twistedmatrix.com> Date: Wed, 4 Sep 2019 13:40:37 -0400 Subject: [PATCH] Probably correct token fingerprinting --- PaymentServer.cabal | 1 + src/PaymentServer/Redemption.hs | 17 +++- test/Driver.hs | 6 +- test/SpecRedemption.hs | 133 ++++++++++++++++++++------------ 4 files changed, 101 insertions(+), 56 deletions(-) diff --git a/PaymentServer.cabal b/PaymentServer.cabal index 8f716de..60a849c 100644 --- a/PaymentServer.cabal +++ b/PaymentServer.cabal @@ -31,6 +31,7 @@ library , stripe-core , text , containers + , cryptonite default-language: Haskell2010 executable PaymentServer-exe diff --git a/src/PaymentServer/Redemption.hs b/src/PaymentServer/Redemption.hs index 8fd54ba..11acd39 100644 --- a/src/PaymentServer/Redemption.hs +++ b/src/PaymentServer/Redemption.hs @@ -20,6 +20,11 @@ import Control.Monad.IO.Class ) import Data.Text ( Text + , pack + ) +import Data.Text.Encoding + ( encodeUtf8 + , decodeUtf8 ) import Data.Aeson ( ToJSON(toJSON, toEncoding) @@ -43,6 +48,10 @@ import Servant.API , ReqBody , (:>) ) +import Crypto.Hash + ( SHA3_512(SHA3_512) + , hashWith + ) import PaymentServer.Persistence ( VoucherDatabase(redeemVoucher) , Fingerprint @@ -86,8 +95,12 @@ redeem database (Redeem voucher tokens) = do let fingerprint = fingerprintFromTokens tokens result <- liftIO $ PaymentServer.Persistence.redeemVoucher database voucher fingerprint case result of - Left err -> return Failed + Left err -> throwError jsonErr400 Right () -> return Succeeded fingerprintFromTokens :: [BlindedToken] -> Fingerprint -fingerprintFromTokens _ = "fingerprint" +fingerprintFromTokens = + let + hash = pack . show . hashWith SHA3_512 . encodeUtf8 + in + foldl (\b a -> hash $ a `mappend` b) "" . map hash diff --git a/test/Driver.hs b/test/Driver.hs index 6462a8a..258e02f 100644 --- a/test/Driver.hs +++ b/test/Driver.hs @@ -31,11 +31,9 @@ tests = do t3 <- pure $ QC.testProperty "getVoucherWithoutVoucher" SpecStripe.prop_getVoucherWithoutVoucher - t4 <- HS.testSpec "simple" SpecRedemption.spec_simple + t4 <- HS.testSpec "redemption" SpecRedemption.spec_redemption - t5 <- HS.testSpec "memory db" SpecRedemption.spec_memory_db - - pure $ T.testGroup "./test/Driver.hs" [t0,t1,t2,t3,t4,t5] + pure $ T.testGroup "./test/Driver.hs" [t0,t1,t2,t3,t4] ingredients :: [T.Ingredient] ingredients = T.defaultIngredients main :: IO () diff --git a/test/SpecRedemption.hs b/test/SpecRedemption.hs index 0f6879b..7c545b9 100644 --- a/test/SpecRedemption.hs +++ b/test/SpecRedemption.hs @@ -5,6 +5,9 @@ module SpecRedemption where +import Data.ByteString + ( ByteString + ) import Text.Printf ( printf ) @@ -31,7 +34,9 @@ import Test.Hspec , runIO ) import Test.Hspec.Wai - ( with + ( ResponseMatcher(ResponseMatcher) + , WaiExpectation + , with , shouldRespondWith , liftIO ) @@ -39,7 +44,8 @@ import Test.Hspec.Wai.QuickCheck ( property ) import Test.QuickCheck - ( (==>) + ( Property + , (==>) ) import Test.QuickCheck.Monadic ( pre @@ -60,7 +66,8 @@ import PaymentServer.Redemption , redemptionServer ) import PaymentServer.Persistence - ( Voucher + ( RedeemError(NotPaid) + , Voucher , Fingerprint , VoucherDatabase(payForVoucher, redeemVoucher) , MemoryVoucherDatabase @@ -75,50 +82,76 @@ app = serve redemptionAPI . redemptionServer path = "/" -spec_simple :: Spec -spec_simple = with (app <$> memory) $ parallel $ do - describe (printf "error behavior of POST %s" (show path)) $ do - wrongMethodNotAllowed "GET" path - nonJSONUnsupportedMediaType path - wrongJSONInvalidRequest path "{}" - -withConnection :: VoucherDatabase d => IO d -> ((d -> IO ()) -> IO ()) -withConnection getDB = bracket getDB (\db -> return ()) - -make_spec_db :: VoucherDatabase d => IO d -> Spec -make_spec_db getDatabase = do - -- Create the database so we can interact with it directly in the tests - -- below. - database <- runIO getDatabase - before (return $ app database) $ - describe "redemption attempts on the server" $ do - it "receive 400 (Invalid Request) when the voucher is unpaid" $ - property $ \(voucher :: Voucher) (tokens :: [BlindedToken]) -> - postJSON path (encode $ Redeem voucher tokens) `shouldRespondWith` 400 - - it "receive 200 (OK) when the voucher is paid" $ - property $ \(voucher :: Voucher) (tokens :: [BlindedToken]) -> - do - liftIO $ payForVoucher database voucher - postJSON path (encode $ Redeem voucher tokens) `shouldRespondWith` 200 - - it "receive 200 (OK) when the voucher is paid and previously redeemed with the same tokens" $ - property $ \(voucher :: Voucher) (tokens :: [BlindedToken]) -> - do - liftIO $ payForVoucher database voucher - postJSON path (encode $ Redeem voucher tokens) `shouldRespondWith` 200 - postJSON path (encode $ Redeem voucher tokens) `shouldRespondWith` 200 - - it "receive 400 (OK) when the voucher is paid and previously redeemed with different tokens" $ - property $ \(voucher :: Voucher) (firstTokens :: [BlindedToken]) (secondTokens :: [BlindedToken]) -> - do - liftIO $ payForVoucher database voucher - postJSON path (encode $ Redeem voucher firstTokens) `shouldRespondWith` 200 - postJSON path (encode $ Redeem voucher secondTokens) `shouldRespondWith` 400 - - - - -spec_memory_db :: Spec -spec_memory_db = - make_spec_db memory +propertyRedeem :: ByteString -> Voucher -> [BlindedToken] -> ResponseMatcher -> WaiExpectation +propertyRedeem path voucher tokens matcher = + postJSON path (encode $ Redeem voucher tokens) `shouldRespondWith` matcher + +-- | A VoucherDatabaseTestDouble has a VoucherDatabase instance which provides +-- a number of different behaviors which are useful to be able to directly +-- test against. +data VoucherDatabaseTestDouble + -- | A RefuseRedemption database always refuses redemption with a given error. + = RefuseRedemption RedeemError + -- | A PermitRedemption database always permits redemption. + | PermitRedemption + deriving (Show) + +instance VoucherDatabase VoucherDatabaseTestDouble where + payForVoucher _ voucher = return () + redeemVoucher (RefuseRedemption err) _ _ = return $ Left err + redeemVoucher PermitRedemption _ _ = return $ Right () + +spec_redemption :: Spec +spec_redemption = parallel $ do + database <- runIO memory + with (return . app $ database) $ + do + describe (printf "error behavior of POST %s" (show path)) $ + do + wrongMethodNotAllowed "GET" path + nonJSONUnsupportedMediaType path + wrongJSONInvalidRequest path "{}" + + -- I would rather write these two as property tests but I don't know + -- how. + describe "double redemption" $ do + it "succeeds with the same tokens" $ do + let voucher = "abc" :: Voucher + let tokens = [ "def", "ghi" ] :: [BlindedToken] + liftIO $ payForVoucher database voucher + propertyRedeem path voucher tokens 200 + propertyRedeem path voucher tokens 200 + + it "fails with different tokens" $ do + let voucher = "jkl" :: Voucher + let firstTokens = [ "mno", "pqr" ] :: [BlindedToken] + let secondTokens = [ "stu", "vwx" ] :: [BlindedToken] + liftIO $ payForVoucher database voucher + propertyRedeem path voucher firstTokens 200 + propertyRedeem path voucher secondTokens 400 + + + describe "redemption" $ do + with (return . app $ RefuseRedemption NotPaid) $ + it "receives 400 (Invalid Request) when the voucher is not paid" $ property $ + \(voucher :: Voucher) (tokens :: [BlindedToken]) -> + propertyRedeem path voucher tokens 400 + + with (return $ app PermitRedemption) $ + it "receive 200 (OK) when redemption succeeds" $ property $ + \(voucher :: Voucher) (tokens :: [BlindedToken]) -> + propertyRedeem path voucher tokens 200 + + -- it "receive 200 (OK) when the voucher is paid and previously redeemed with the same tokens" $ + -- property $ \(voucher :: Voucher) (tokens :: [BlindedToken]) -> + -- do + -- liftIO $ payForVoucher database voucher + -- postJSON path (encode $ Redeem voucher tokens) `shouldRespondWith` 200 + -- postJSON path (encode $ Redeem voucher tokens) `shouldRespondWith` 200 + + -- it "receive 400 (OK) when the voucher is paid and previously redeemed with different tokens" $ + -- property $ \(voucher :: Voucher) (firstTokens :: [BlindedToken]) (secondTokens :: [BlindedToken]) -> + -- do + -- liftIO $ payForVoucher database voucher + -- postJSON path (encode $ Redeem voucher firstTokens) `shouldRespondWith` 200 + -- postJSON path (encode $ Redeem voucher secondTokens) `shouldRespondWith` 400 -- GitLab