From 0f7c5ffb7a558495c868036853782a4920a8c2f5 Mon Sep 17 00:00:00 2001 From: "Evgeny @ SimpleX Chat" <259188159+evgeny-simplex@users.noreply.github.com> Date: Wed, 15 Jul 2026 08:48:24 +0000 Subject: [PATCH] sign address card when shared by owner --- src/Simplex/Chat/Library/Commands.hs | 49 +++++++++++++++------------- tests/ChatTests/Profiles.hs | 39 ++++++++++++++++++++-- 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/Simplex/Chat/Library/Commands.hs b/src/Simplex/Chat/Library/Commands.hs index 3b579d3cc3..28ad54f7c8 100644 --- a/src/Simplex/Chat/Library/Commands.hs +++ b/src/Simplex/Chat/Library/Commands.hs @@ -1144,39 +1144,23 @@ processChatCommand cxt nm = \case _ -> Nothing ownerSig <- pure signingKeys $>>= \GroupKeys {memberPrivKey} -> - mkLinkOwnerSig memberPrivKey groupLink memberId <$$> shareChatBinding user toSendRef + mkLinkOwnerSig memberPrivKey groupLink (Just memberId) <$$> shareChatBinding user toSendRef let text = safeDecodeUtf8 $ strEncode groupLink pure $ CRChatMsgContent user MCChat {text, chatLink = MCLGroup groupLink gp, ownerSig} - where - mkLinkOwnerSig :: ConnectionModeI m => C.PrivateKeyEd25519 -> ConnShortLink m -> MemberId -> (ChatBinding, ByteString) -> LinkOwnerSig - mkLinkOwnerSig privKey connLink MemberId {unMemberId} (cbTag, bindingData) = - let ownerId = Just $ B64UrlByteString unMemberId - cb = encodeChatBinding cbTag bindingData - ownerSig = C.sign' privKey $ cb <> smpEncode connLink - in LinkOwnerSig {ownerId, chatBinding = B64UrlByteString cb, ownerSig} - shareChatBinding :: User -> SendRef -> CM (Maybe (ChatBinding, ByteString)) - shareChatBinding u = \case - SRDirect contactId -> do - ct <- withFastStore $ \db -> getContact db cxt u contactId - forM (contactConn ct) $ \conn -> - (CBDirect,) <$> withAgent (`getConnectionRatchetAdHash` aConnId conn) - SRGroup toGroupId _ asGroup -> do - GroupInfo {groupProfile = GroupProfile {publicGroup}, membership = m} <- withFastStore $ \db -> getGroupInfo db cxt u toGroupId - pure $ mkBinding m <$> publicGroup - where - mkBinding GroupMember {memberId} PublicGroupProfile {publicGroupId = pgId} - | asGroup = (CBChannel, smpEncode pgId) - | otherwise = (CBGroup, smpEncode (pgId, memberId)) APIShareChatMsgContent _ _ -> throwCmdError "sharing is only supported for public groups" - APIShareMyAddress _ -> withUser $ \user -> do + APIShareMyAddress toSendRef -> withUser $ \user -> do UserContactLink {connLinkContact = CCLink _ sl_, addressSettings} <- withFastStore (`getUserAddress` user) case sl_ of Nothing -> throwCmdError "your address has no short link to share" Just connLink -> do + conn <- withFastStore $ \db -> getUserAddressConnection db cxt user + ownerSig <- + withAgent (`getConnLinkPrivKey` aConnId conn) $>>= \privKey -> + mkLinkOwnerSig privKey connLink Nothing <$$> shareChatBinding user toSendRef let business = businessAddress addressSettings profile = userProfileDirect user Nothing Nothing True text = safeDecodeUtf8 $ strEncode connLink - pure $ CRChatMsgContent user MCChat {text, chatLink = MCLContact {connLink, profile, business}, ownerSig = Nothing} + pure $ CRChatMsgContent user MCChat {text, chatLink = MCLContact {connLink, profile, business}, ownerSig} APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user UserRead -> withUser $ \User {userId} -> processChatCommand cxt nm $ APIUserRead userId APIChatRead chatRef@(ChatRef cType chatId scope_) -> withUser $ \_ -> case cType of @@ -4537,6 +4521,25 @@ processChatCommand cxt nm = \case serverShortLink = \case CSLInvitation _ srv lnkId linkKey -> CSLInvitation SLSServer srv lnkId linkKey CSLContact _ ct srv linkKey -> CSLContact SLSServer ct srv linkKey + mkLinkOwnerSig :: ConnectionModeI m => C.PrivateKeyEd25519 -> ConnShortLink m -> Maybe MemberId -> (ChatBinding, ByteString) -> LinkOwnerSig + mkLinkOwnerSig privKey connLink ownerMemberId (cbTag, bindingData) = + let ownerId = (\MemberId {unMemberId} -> B64UrlByteString unMemberId) <$> ownerMemberId + cb = encodeChatBinding cbTag bindingData + ownerSig = C.sign' privKey $ cb <> smpEncode connLink + in LinkOwnerSig {ownerId, chatBinding = B64UrlByteString cb, ownerSig} + shareChatBinding :: User -> SendRef -> CM (Maybe (ChatBinding, ByteString)) + shareChatBinding u = \case + SRDirect contactId -> do + ct <- withFastStore $ \db -> getContact db cxt u contactId + forM (contactConn ct) $ \conn -> + (CBDirect,) <$> withAgent (`getConnectionRatchetAdHash` aConnId conn) + SRGroup toGroupId _ asGroup -> do + GroupInfo {groupProfile = GroupProfile {publicGroup}, membership = m} <- withFastStore $ \db -> getGroupInfo db cxt u toGroupId + pure $ mkBinding m <$> publicGroup + where + mkBinding GroupMember {memberId} PublicGroupProfile {publicGroupId = pgId} + | asGroup = (CBChannel, smpEncode pgId) + | otherwise = (CBGroup, smpEncode (pgId, memberId)) verifyLinkOwner :: ConnectionModeI m => C.PublicKeyEd25519 -> [OwnerAuth] -> ConnShortLink m -> Maybe LinkOwnerSig -> Maybe OwnerVerification verifyLinkOwner rootKey owners connLink = fmap $ \LinkOwnerSig {ownerId, chatBinding = B64UrlByteString bindingBytes, ownerSig} -> diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index e66fee0640..28bb66261a 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -26,9 +26,9 @@ import qualified Data.Map.Strict as M import Simplex.Chat.Badges (BadgeCredential, BadgeInfo (..), BadgePurchase (..), BadgeRequest (..), BadgeType (..), generateMasterKey, issueBadge, verifyPayment) import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatHooks (..), defaultChatHooks, mkStoreCxt) import Simplex.Chat.Options (ChatOpts (..), CoreChatOpts (..)) -import Simplex.Chat.Protocol (currentChatVersion) +import Simplex.Chat.Protocol (LinkOwnerSig, MsgChatLink (..), MsgContent (..), currentChatVersion) import Simplex.Chat.Store.Shared (createContact) -import Simplex.Chat.Types (ConnStatus (..), Profile (..), GroupRejectionReason (..)) +import Simplex.Chat.Types (ConnStatus (..), Profile (..), GroupRejectionReason (..), profileFromName) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.BBS (BBSPublicKey, BBSSecretKey, bbsKeyGen) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) @@ -38,7 +38,7 @@ import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.Messaging.Server.Env.STM hiding (subscriptions) import Simplex.Messaging.Transport -import Simplex.Messaging.Util (encodeJSON) +import Simplex.Messaging.Util (decodeJSON, encodeJSON) import System.Directory (copyFile, createDirectoryIfMissing) import Test.Hspec hiding (it) @@ -121,6 +121,7 @@ chatProfileTests = do it "should connect via one-time invitation" testShortLinkInvitation it "should plan and connect via one-time invitation" testPlanShortLinkInvitation it "should connect via contact address" testShortLinkContactAddress + it "should share contact address via chat" testShareAddressViaChat it "should join group" testShortLinkJoinGroup describe "short links with attached data" shortLinkTests describe "client services" $ do @@ -3026,6 +3027,38 @@ testPlanShortLinkInvitation = slSimplexScheme :: String -> String slSimplexScheme sl = T.unpack $ T.replace "https://localhost/" "simplex:/" (T.pack sl) <> "?h=localhost" +testShareAddressViaChat :: HasCallStack => TestParams -> IO () +testShareAddressViaChat = + testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + alice ##> "/ad" + _ <- getContactLinks alice True + connectUsers alice bob + connectUsers bob cath + -- alice shares her signed address card to bob + alice ##> "/share address @bob" + alice <# "@bob contact address of @alice (signed):" + _ <- getTermLine alice -- link + _ <- getTermLine alice -- owner signature (testView) + bob <# "alice> contact address of @alice (signed):" + bLink <- getTermLine bob + bSig <- getTermLine bob + -- bob verifies alice's owner signature + bob ##> ("/_connect plan 1 " <> bLink <> " sig=" <> bSig) + bob <## "contact address: ok to connect" + bob <## "owner signature: verified" + _ <- getTermLine bob -- link data + -- bob replays alice's signed card to cath: the binding was alice->bob, so cath strips the signature + let sig = maybe (error "bad sig") id (decodeJSON (T.pack bSig) :: Maybe LinkOwnerSig) + cLink = either error id $ strDecode (B.pack bLink) + mc = MCChat (T.pack bLink) (MCLContact cLink (profileFromName "alice") False) (Just sig) + cm = "{\"msgContent\":" <> T.unpack (encodeJSON mc) <> "}" + bob ##> ("/_send @3 json [" <> cm <> "]") + bob <# "@cath contact address of @alice (signed):" + _ <- getTermLine bob -- link + _ <- getTermLine bob -- owner signature (bob's sent view) + cath <# "bob> contact address of @alice:" + void $ getTermLine cath -- link (signature stripped) + testShortLinkContactAddress :: HasCallStack => TestParams -> IO () testShortLinkContactAddress = testChat4 aliceProfile bobProfile cathProfile danProfile $ \alice bob cath dan -> do