{-# LANGUAGE DataKinds #-} {-# LANGUAGE DisambiguateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} module BadgeTests (badgeTests) where import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Time.Clock (UTCTime, addUTCTime, getCurrentTime, nominalDay) import Data.Time.Clock.POSIX (posixSecondsToUTCTime) import qualified Data.Aeson as J import qualified Simplex.Messaging.Crypto as C import Simplex.Chat.Badges import Simplex.Messaging.Crypto.BBS import Test.Hspec badgeTests :: Spec badgeTests = do it "full workflow: request, issue, verify credential, generate and verify proof" testFullWorkflow it "should reject badge with tampered type" testTamperedType it "should reject badge with tampered expiry" testTamperedExpiry it "should reject badge with wrong server key" testWrongKey it "should report a key index missing from configured keys" testUnknownKeyIdx it "should compute badge status correctly" testExpiryCheck it "should treat lifetime badges as always active" testLifetimeBadge it "should accept unknown badge types" testUnknownBadgeType it "credential serializes to a paste-able token and back" testCredentialSerialization proofOf :: BadgeProof -> BBSProof proofOf (BadgeProof _ _ p _) = p proofInfo :: BadgeProof -> BadgeInfo proofInfo (BadgeProof _ _ _ i) = i testKeyIdx :: Int testKeyIdx = 1 keysFor :: BBSPublicKey -> Map Int BBSPublicKey keysFor = M.singleton testKeyIdx testFullWorkflow :: IO () testFullWorkflow = do Right (pk, sk) <- bbsKeyGen drg <- C.newRandom mk <- generateMasterKey drg let req = BadgeRequest {masterKey = mk, badgeInfo = BadgeInfo {badgeType = BTSupporter, badgeExpiry = Just futureTime, badgeExtra = ""}} Just vreq <- verifyPayment (BPRedeemCode "TEST") req Right cred <- issueBadge testKeyIdx sk vreq let BadgeCredential idx mk' _ _ = cred idx `shouldBe` testKeyIdx mk' `shouldBe` mk verifyCredential pk cred >>= (`shouldBe` True) Right badge <- generateBadgeProof pk cred (BBSPresHeader "nonce-1") -- the proof inherits the credential's key index, so receivers find the right key let BadgeProof {badgeKeyIdx} = badge badgeKeyIdx `shouldBe` testKeyIdx verifyBadge (keysFor pk) badge >>= (`shouldBe` Just True) Right badge2 <- generateBadgeProof pk cred (BBSPresHeader "nonce-2") verifyBadge (keysFor pk) badge2 >>= (`shouldBe` Just True) proofOf badge `shouldNotBe` proofOf badge2 testTamperedType :: IO () testTamperedType = do (pk, BadgeProof idx ph p info) <- issueBadgeProof BTSupporter (Just futureTime) verifyBadge (keysFor pk) (BadgeProof idx ph p info {badgeType = BTLegend}) >>= (`shouldBe` Just False) testTamperedExpiry :: IO () testTamperedExpiry = do (pk, BadgeProof idx ph p info) <- issueBadgeProof BTSupporter (Just futureTime) verifyBadge (keysFor pk) (BadgeProof idx ph p info {badgeExpiry = Just pastTime}) >>= (`shouldBe` Just False) testWrongKey :: IO () testWrongKey = do (_, badge) <- issueBadgeProof BTSupporter (Just futureTime) Right (pk2, _) <- bbsKeyGen verifyBadge (keysFor pk2) badge >>= (`shouldBe` Just False) testUnknownKeyIdx :: IO () testUnknownKeyIdx = do (pk, badge) <- issueBadgeProof BTSupporter (Just futureTime) -- a key index not in the configured keys cannot be verified at all (Nothing) verifyBadge (M.singleton (testKeyIdx + 1) pk) badge >>= (`shouldBe` Nothing) testExpiryCheck :: IO () testExpiryCheck = do now <- getCurrentTime let info expiry = BadgeInfo {badgeType = BTSupporter, badgeExpiry = expiry, badgeExtra = ""} futureInfo = info (Just futureTime) mkBadgeStatus now (Just True) futureInfo `shouldBe` BSActive mkBadgeStatus now (Just True) (info (Just (addUTCTime (-nominalDay) now))) `shouldBe` BSExpired mkBadgeStatus now (Just True) (info (Just pastTime)) `shouldBe` BSExpiredOld mkBadgeStatus now (Just False) futureInfo `shouldBe` BSFailed mkBadgeStatus now Nothing futureInfo `shouldBe` BSUnknownKey testLifetimeBadge :: IO () testLifetimeBadge = do now <- getCurrentTime (pk, badge) <- issueBadgeProof BTInvestor Nothing verifyBadge (keysFor pk) badge >>= (`shouldBe` Just True) mkBadgeStatus now (Just True) (proofInfo badge) `shouldBe` BSActive testUnknownBadgeType :: IO () testUnknownBadgeType = do (pk, badge) <- issueBadgeProof (BTUnknown "future_type") (Just futureTime) verifyBadge (keysFor pk) badge >>= (`shouldBe` Just True) testCredentialSerialization :: IO () testCredentialSerialization = do Right (pk, sk) <- bbsKeyGen drg <- C.newRandom mk <- generateMasterKey drg let mkCred expiry = do Right cred <- issueBadge testKeyIdx sk (VerifiedBadgeRequest BadgeRequest {masterKey = mk, badgeInfo = BadgeInfo {badgeType = BTSupporter, badgeExpiry = expiry, badgeExtra = ""}}) pure cred dated <- mkCred (Just futureTime) lifetime <- mkCred Nothing J.eitherDecode (J.encode dated) `shouldBe` Right dated J.eitherDecode (J.encode lifetime) `shouldBe` Right lifetime -- a decoded credential still verifies against the issuing key case J.eitherDecode (J.encode dated) of Right cred -> verifyCredential pk cred >>= (`shouldBe` True) Left e -> expectationFailure e -- Helpers futureTime :: UTCTime futureTime = posixSecondsToUTCTime 4102444800 -- 2099-12-31 pastTime :: UTCTime pastTime = posixSecondsToUTCTime 1577836800 -- 2020-01-01 issueBadgeProof :: BadgeType -> Maybe UTCTime -> IO (BBSPublicKey, BadgeProof) issueBadgeProof bt expiry = do Right (pk, sk) <- bbsKeyGen drg <- C.newRandom mk <- generateMasterKey drg let vreq = VerifiedBadgeRequest BadgeRequest {masterKey = mk, badgeInfo = BadgeInfo {badgeType = bt, badgeExpiry = expiry, badgeExtra = ""}} Right cred <- issueBadge testKeyIdx sk vreq Right badge <- generateBadgeProof pk cred (BBSPresHeader "test-nonce") pure (pk, badge)