Files
Evgeny ad23da63d0 core: supporter badges using anonymous BBS credentials (#7040)
* core: supporter badges using anonymous BBS credentials

* badges in profiles

* badge in profiles

* process badges

* update simplexmq

* update simplexmq

* change types

* fix migration

* migration

* update simplexmq

* fix bot API, schema

* fix postgresql build

* refactor

* postgresql schema

* correctly set badges in all cases

* badges ffi

* plan, bot types

* FFI

* FFI: export badge symbols

* add extra field

* refactor badge types to GADT

* configurable badge key

* add badge to profile, test

* ui: badge images

* generate badge key and sign badge

* badge sign in CLI

* fix commands, ui

* rename badges

* Binary

* image size, migration

* update badge images, add public key

* send badges in more cases

* update UI, tests

* bot types, schema

* postgres schema

* tone down badges

* revert formula

* refactor badges

* smaller badges

* badge position

* better badge position

* simpler

* position

* move position

* update simplexmq

* show badge after name

* badge layout

* fix badge

* debug badge height

* shift badge

* fix badge in member name

* bigger badge

* badge layout

* differentiate badge colors

* more avatars for the user's profiles

* refactor

* remove color filter

* alerts

* multiple keys, old expired

* use new BBS api

* update badge keys, bot api

* presentation header

* simplify

* parser

* update iOS images

* update public keys

* query plans

* update simplexmq

* refactor badge types

* simplexmq

* bot api types

* update simplexmq - commoncrypto flag

* update simplexmq

* pass commoncrypto flag to simplexmq in nix iOS build

* ios ui

* update core library, fixes

* badge layout

* badge size

* badge gap

* remove extensions

* simplify

* share badge in more events, reverify badge if verification failed

* larger files with badges

* allow sending larger files

* simpler

* update simplexmq

* better decoder for badge keys

* update simplexmq

---------

Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com>
Co-authored-by: shum <github.shum@liber.li>
2026-06-15 22:25:08 +01:00

143 lines
5.7 KiB
Haskell

{-# 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)