Skip to content

Commit b7a9542

Browse files
smp server: short links and owners for channels (#1506)
* smp server: short links and owners for channels * types * support mutliple rcv keys * fix down migration, test/create server schema dump * reduce schema dump * parameterize type for link data by connection type * return full connection link data * test version * change short link encoding * test: print pg_dump output * server pages, link encoding * fix connection request when queue data and sender ID are created for old servers * test, change pattern * ci: install postgresql tools in runner (#1507) * ci: install postgresql tools in runner * ci: docker shell abort on error * fix pattern for ghc 8.10.7 * patch ConnReqUriData SMP encoding to preserve queue mode after decoding * test for RKEY * fix/test store log with RKEY --------- Co-authored-by: sh <[email protected]>
1 parent 3a3f9fd commit b7a9542

32 files changed

+869
-268
lines changed

.github/workflows/build.yml

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ jobs:
4848
- name: Set up Docker Buildx
4949
uses: simplex-chat/docker-setup-buildx-action@v3
5050

51+
- name: Install PostgreSQL 15 client tools
52+
if: matrix.os == '22.04'
53+
shell: bash
54+
run: |
55+
# Import the repository signing key
56+
sudo install -d /usr/share/postgresql-common/pgdg
57+
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
58+
# Add the PostgreSQL APT repository
59+
sudo sh -c 'echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
60+
# Update repository and install postgresql tools
61+
sudo apt update
62+
sudo apt -y install postgresql-client-15
63+
5164
- name: Build and cache Docker image
5265
uses: simplex-chat/docker-build-push-action@v6
5366
with:
@@ -82,7 +95,7 @@ jobs:
8295
build/${{ matrix.platform_name }}:latest
8396
8497
- name: Build smp-server (postgresql) and tests
85-
shell: docker exec -t builder sh {0}
98+
shell: docker exec -t builder sh -eu {0}
8699
run: |
87100
cabal update
88101
cabal build --jobs=$(nproc) --enable-tests -fserver_postgres
@@ -106,7 +119,7 @@ jobs:
106119
docker cp builder:/out/smp-server ./smp-server-postgres-ubuntu-${{ matrix.platform_name }}
107120
108121
- name: Build everything else (standard)
109-
shell: docker exec -t builder sh {0}
122+
shell: docker exec -t builder sh -eu {0}
110123
run: |
111124
cabal build --jobs=$(nproc)
112125
mkdir -p /out
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
./apps/smp-server/static/link.html
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
./apps/smp-server/static/link.html
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
./apps/smp-server/static/link.html

apps/smp-server/web/Static.hs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,16 @@ generateSite si onionHost sitePath = do
8484
B.writeFile (sitePath </> "index.html") $ serverInformation si onionHost
8585
createDirectoryIfMissing True $ sitePath </> "media"
8686
forM_ E.mediaContent $ \(path, bs) -> B.writeFile (sitePath </> "media" </> path) bs
87-
createDirectoryIfMissing True $ sitePath </> "contact"
88-
B.writeFile (sitePath </> "contact" </> "index.html") E.linkHtml
89-
createDirectoryIfMissing True $ sitePath </> "invitation"
90-
B.writeFile (sitePath </> "invitation" </> "index.html") E.linkHtml
87+
createLinkPage "contact"
88+
createLinkPage "invitation"
89+
createLinkPage "a"
90+
createLinkPage "c"
91+
createLinkPage "i"
9192
logInfo $ "Generated static site contents at " <> tshow sitePath
93+
where
94+
createLinkPage path = do
95+
createDirectoryIfMissing True $ sitePath </> path
96+
B.writeFile (sitePath </> path </> "index.html") E.linkHtml
9297

9398
serverInformation :: ServerInformation -> Maybe TransportHost -> ByteString
9499
serverInformation ServerInformation {config, information} onionHost = render E.indexHtml substs

rfcs/2025-04-04-short-links-for-groups.md

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -59,45 +59,101 @@ While the server domain would be used as the hostname in group link, it may cont
5959
Pros: separates additional complexity to where it is needed, allowing reliability and redundancy for group ownership.
6060
Cons: complexity, coupling between SMP and chat protocol.
6161

62-
## Design ideas for group as a separate entity type
62+
## Design for channel/group as a separate queue mode
6363

64-
The last solution approach seems both the most long-term and also provides the best functionality, so maybe it could be an extensible base.
64+
Option 1.
6565

66-
Its advantage is that it does not require e2e encryption between owners, as only public keys are shared (although MITM is still possible without verification, mitigated by using multiple chat relays).
66+
A queue mode "channel" when owners are represented by their individual queues (either a separate mode, or a submode of "channel", or just normal contact address queues). In this case sending message to channel queue would broadcast message to queue owners, without exposing even the number of owners.
6767

68-
The proposal is to have a new entity "asset", and a queue mode "asset". This entity represents a reference to some kind of digital asset, not part of SMP spec. Each link is managed by multiple owners, each represented with "asset" queue.
68+
Pros:
69+
- allows chat relays to send messages to all owners (e.g., channel can be secured with the list of snd keys, one per relay).
70+
- quite easy to evolve from the current design.
71+
- extensible.
72+
Cons:
73+
- close to "solution in search of a problem".
74+
- does not require data model changes - channel queue would simply have a list of owner "recipient IDs", and each owner queue would also point to channel.
75+
76+
Option 2.
77+
78+
Also a separate queue mode "channel", but instead of having a linked owner queues, it would simply maintain a list of owner keys to maintain the data. In this case, messages cannot be sent to this "queue" at all.
79+
80+
Pros:
81+
- simpler design.
82+
- we could allow sending messages to it too, with the "main" owner receiving them. This could be negotiated in the protocol.
83+
- it may be easier to migrate the current groups, as the admin link would be this queue (although for public groups in directory it would have to be recreated anyway).
84+
- Possibly, when queue is created there should be a flag whether it should accept unsigned messages - then contact addresses would be created with unsigned messages ON, messages queues, once SKEY is universally supported, with unsigned messages OFF, and channel queues with unsigned messages OFF too for new public queues.
85+
Cons:
86+
- if no messages are accepted, this is not even a queue.
87+
- no way to directly contact owners (maybe it is not a downside, as for relays there would be a communication channel anyway as part of the group).
88+
89+
Option 2 looks more simple and attractive, implementing server broadcast for SMP seems unnecessary, as while it could have been used for simple groups, it does not solve such problems as spam and pre-moderation anyway - it requires a higher level protocol.
90+
91+
The command to update owner keys would be `RKEY` with the list of keys, and we can make `NEW` accept multiple keys too, although the use case here is less clear.
92+
93+
## Multiple owners managing queue data.
94+
95+
Option 1: Use the same keys in SMP as when signing queue data.
96+
97+
Option 2: Use different keys.
98+
99+
The value here could be that the server could validate these signatures too, and also maintain the chain of key changes. While tempting, it is probably unnecessary, and this chain of ownership is better to be maintained on chat relay level, as there are no size constraints on the size of this chain. Also, it is better for metadata privacy to not couple transport and chat protocol keys.
100+
101+
We still need to bind the mutable data updates to the "genesis" signature key (the one included in the immutable data).
102+
103+
The proposed design:
69104

70-
The change from the current design is simple - splitting the link and data from the queue table/record into a separate table, so that the same link can be referenced by multiple queues with different owner IDs. Any of the linked asset queue recipients can modify link data and delete link. The protocol encoding could support multisig, but it can be added later with a separate protocol version. But it would alread allow having multiple owners for link.
105+
- when mutable data is signed by genesis key, then it is bound, and no changes is needed.
106+
- mutable data may be signed by the key of the new owner, in which case mutable part itself must contain the binding. We could also use ring signature to sign the mutable data, concealing which owner signed the data - that would increase the signature size from 64 bytes to `32 * (n + 1)` bytes.
71107

72-
It is also probably correct to require that Ed25519 is used for recipient/owner authorization (and not X25519 authenticators that are used for senders).
108+
Current mutable data:
73109

74-
Question 1: should the same signature key be used for signing server commands and owner-owner comms? There may be a benefit in having two different keys, and in contexts visible to both server and owners use server key, and in contexts visible to owners only use signature key inside data.
110+
```haskell
111+
data UserLinkData = UserLinkData
112+
{ agentVRange :: VersionRangeSMPA,
113+
userData :: ConnInfo
114+
}
115+
```
75116

76-
Question 2: should non-owners see server keys of owners? If not, does it suggest a third owner-only data blob? Or should the server simply maintain the currently signed ownership agreement? Or even the history of the agreement changes?
117+
Proposed mutable data:
77118

78-
Question 3: how would the owner validate the correctness of ownership changes - where this chain will be maintained? Should it maybe be replicated to all owners' "asset" queues? Or will it be a separate "chain" that will be truncated once all owners acknowledge the change? Almost like a separate queue?
119+
```haskell
120+
data UserLinkData = UserLinkData
121+
{ agentVRange :: VersionRangeSMPA,
122+
owners :: [OwnerInfo]
123+
userData :: ConnInfo
124+
}
79125

80-
The protocol change required would be to make sender ID optional in LNK response. Alternatively, link could have its own sender ID and broadcast messages to link owners, and a rule whether messages can be sent without key, and whether this link can be secured with SKEY. Depending on queue type it would be:
126+
type OwnerId = ByteString
81127

82-
- "messaging" queue: can secure, can send messages.
83-
- "contact" queue: cannot secure (only owner can secure), can send unsigned messages.
84-
- "asset" queue: cannot secure, cannot send unsigned messages.
128+
data OwnerInfo = OwnerInfo
129+
{ ownerId :: OwnerId, -- unique in the list, application specific - e.g., MemberId
130+
ownerKey :: PublicKeyEd25519,
131+
-- owner signature of sender ID,
132+
-- confirms that the owner agreed with being the owner,
133+
-- prevents a member being added as an owner without consent.
134+
ownerSig :: SignatureEd25519,
135+
-- owner authorization, sig(ownerId || ownerKey, prevKey), where prevKey is either a "genesis key" or some other key previously signed by the genesis key.
136+
authOwnerId :: OwnerId, -- null for "genesis"
137+
authOwnerSig :: SignatureEd25519
138+
}
139+
```
85140

86-
To allow multiple delegates the queue could allow multiple send keys. In case of delegates (chat relays), we could require that only Ed25519 keys are used (for non-repudiation).
141+
The size of the OwnerInfo record encoding is:
142+
- ownerId: 1 + 12
143+
- ownerKey: 1 + 32
144+
- ownerSig: 1 + 64
145+
- ownerAuthId: 1 + 12
146+
- ownerAuthSig: 1 + 64
87147

88-
The additional commands required would be to add, get and delete link owners:
89-
- invite owner (OADD): adds some random server-generated new owner ID to link - this token with the current owner's signature will be included in NEW command (the signed token to be passed out of band). Separate table?
90-
- remove owner (ODEL): remove owner from link by owner ID (both before and after new owner accepted ownership).
91-
- get owners (OGET): get current owner IDs and their public keys.
92-
- how would notification be delivered to the owner when s/he is removed? Some event? Possibly all changes are delivered as messages to each "owner's" asset queue, probably with longer expiration periods?
148+
~189 bytes, so we should practically limit the number of owners to say 8 - 1 original + 7 addiitonal. Original creator could use a different key as a "genesis" key, to conceal creator identity from other members, and it needs to include the record with memberId anyway.
93149

94-
While initially we don't need to build support for multisig in UX, it can be easily added later with this design.
150+
The structure is simplified, and it does not allow arbitrary ownership changes. Its purpose is not to comprehensively manage ownership changes - while it is possible with a generic blockchain, it seems not appropriate at this stage, - but rather to ensure access continuity and that the server cannot modify the data (although nothing prevents the server from removing the data completely or from serving the previous version of the data).
95151

96-
The flow then would be, for new "asset" queue with link - it is created as usual, with "NEW" command, and queueMode QMAsset that is passed linkId and link data.
152+
For example it would only allow any given owner to remove subsequenty added owners, preserving the group link and identity, but it won't allow removing owners that signed this owner authorization. So owners are not equal, with the creator having the highest rank and being able to remove all additional owners, and owners authorise by creator can remove all other owners but themselves and creator, and so on - they have to maintain the chain that authorized themselves, at least. We could explicitely include owner rank into OwnerInfo, or we could require that they are sorted by rank, or the rank can be simply derived from signatures.
97153

98-
When additional owners want to be added to the group, they would have to create "link" type queue without link ID (thus preventing non-consensual ownership transfer). The flow would be this:
99-
- group owner(s) offer to become additional owner with the specific new multisig rule, this is sent as a message in chat with signed ID from `OADD` command.
100-
- the proposed owner will validate that this offer is signed according to the current multisig rule, by loading queue data and current owners (possibly via its ID from `OADD` command, that would also secure this owner ID).
101-
- if the proposed owner accepts it, s/he will create a new "asset" queue linking it with the same link ID - the server would also accept signed owner ID as a confirmation.
154+
When additional owners want to be added to the group, they would have to provide any of the current owners:
155+
- the key for SMP commands authorization - this will be passed to SMP server together with other keys. There could be either RKEY to pass all keys (some risk to miss some, or of race conditions), or RADD/RGET/RDEL to add and remove recipient keys, which has no risk of race conditions.
156+
- the signature of the immutable data by their member key included in their profile.
157+
- the current owner would then include their member key into the queue data, and update it with LSET command. In any case there should be some simple consensus protocol between owners for owner changes, and it has to be maintained as a blockchain by owners and by chat relays, as otherwise it may lead to race conditions with LSET command.
102158

103-
Alternatively, it could be an out-of-band exchange first, when existing owner sends an offer, the new owner accepts it and returns the key (and signed offer), and then this key is sent to the server by the old owner, returning owner ID to the new owner.
159+
Potentially, there could be one command to update keys and link data, so that they are consistent.

simplexmq.cabal

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,9 @@ test-suite simplexmq-test
482482
other-modules:
483483
AgentTests.SchemaDump
484484
AgentTests.SQLiteTests
485+
if flag(server_postgres)
486+
other-modules:
487+
ServerTests.SchemaDump
485488
hs-source-dirs:
486489
tests
487490
apps/smp-server/web
@@ -537,11 +540,13 @@ test-suite simplexmq-test
537540
if flag(client_postgres)
538541
cpp-options: -DdbPostgres
539542
else
543+
build-depends:
544+
memory
545+
, sqlcipher-simple
546+
if !flag(client_postgres) || flag(server_postgres)
540547
build-depends:
541548
deepseq ==1.4.*
542-
, memory
543549
, process
544-
, sqlcipher-simple
545550
if flag(client_postgres) || flag(server_postgres)
546551
build-depends:
547552
postgresql-simple ==0.7.*

src/Simplex/Messaging/Agent.hs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ import Simplex.Messaging.Protocol
200200
MsgFlags (..),
201201
NtfServer,
202202
ProtoServerWithAuth (..),
203+
ProtocolServer (..),
203204
ProtocolType (..),
204205
ProtocolTypeI (..),
205206
QueueLinkData,
@@ -379,7 +380,7 @@ deleteContactShortLink c = withAgentEnv c . deleteContactShortLink' c
379380
{-# INLINE deleteContactShortLink #-}
380381

381382
-- | Get and verify data from short link. For 1-time invitations it preserves the key to allow retries
382-
getConnShortLink :: AgentClient -> UserId -> ConnShortLink c -> AE (ConnectionRequestUri c, ConnInfo)
383+
getConnShortLink :: AgentClient -> UserId -> ConnShortLink c -> AE (ConnectionRequestUri c, ConnLinkData c)
383384
getConnShortLink c = withAgentEnv c .: getConnShortLink' c
384385
{-# INLINE getConnShortLink #-}
385386

@@ -838,7 +839,7 @@ setContactShortLink' c connId userData =
838839
SomeConn _ (ContactConnection _ rq) -> do
839840
(lnkId, linkKey, d) <- prepareLinkData rq
840841
addQueueLink c rq lnkId d
841-
pure $ CSLContact (qServer rq) CCTContact linkKey
842+
pure $ CSLContact SLSServer CCTContact (qServer rq) linkKey
842843
_ -> throwE $ CMD PROHIBITED "setContactShortLink: not contact address"
843844
where
844845
prepareLinkData :: RcvQueue -> AM (SMP.LinkId, LinkKey, QueueLinkData)
@@ -870,9 +871,9 @@ deleteContactShortLink' c connId =
870871
_ -> throwE $ CMD PROHIBITED "deleteContactShortLink: not contact address"
871872

872873
-- TODO [short links] remove 1-time invitation data and link ID from the server after the message is sent.
873-
getConnShortLink' :: forall c. AgentClient -> UserId -> ConnShortLink c -> AM (ConnectionRequestUri c, ConnInfo)
874+
getConnShortLink' :: forall c. AgentClient -> UserId -> ConnShortLink c -> AM (ConnectionRequestUri c, ConnLinkData c)
874875
getConnShortLink' c userId = \case
875-
CSLInvitation srv linkId linkKey -> do
876+
CSLInvitation _ srv linkId linkKey -> do
876877
g <- asks random
877878
invLink <- withStore' c $ \db -> do
878879
getInvShortLink db srv linkId >>= \case
@@ -886,20 +887,22 @@ getConnShortLink' c userId = \case
886887
ld@(sndId, _) <- secureGetQueueLink c userId invLink
887888
withStore' c $ \db -> setInvShortLinkSndId db invLink sndId
888889
decryptData srv linkKey k ld
889-
CSLContact srv _ linkKey -> do
890+
CSLContact _ _ srv linkKey -> do
890891
let (linkId, k) = SL.contactShortLinkKdf linkKey
891892
ld <- getQueueLink c userId srv linkId
892893
decryptData srv linkKey k ld
893894
where
894-
decryptData :: ConnectionModeI c => SMPServer -> LinkKey -> C.SbKey -> (SMP.SenderId, QueueLinkData) -> AM (ConnectionRequestUri c, ConnInfo)
895+
decryptData :: ConnectionModeI c => SMPServer -> LinkKey -> C.SbKey -> (SMP.SenderId, QueueLinkData) -> AM (ConnectionRequestUri c, ConnLinkData c)
895896
decryptData srv linkKey k (sndId, d) = do
896897
r@(cReq, _) <- liftEither $ SL.decryptLinkData @c linkKey k d
897-
unless ((srv, sndId) `sameQAddress` qAddress (connReqQueue cReq)) $
898+
let (srv', sndId') = qAddress (connReqQueue cReq)
899+
unless (srv `sameSrvHost` srv' && sndId == sndId') $
898900
throwE $ AGENT $ A_LINK "different address"
899901
pure r
902+
sameSrvHost ProtocolServer {host = h :| _} ProtocolServer {host = hs} = h `elem` hs
900903

901904
deleteLocalInvShortLink' :: AgentClient -> ConnShortLink 'CMInvitation -> AM ()
902-
deleteLocalInvShortLink' c (CSLInvitation srv linkId _) = withStore' c $ \db -> deleteInvShortLink db srv linkId
905+
deleteLocalInvShortLink' c (CSLInvitation _ srv linkId _) = withStore' c $ \db -> deleteInvShortLink db srv linkId
903906

904907
changeConnectionUser' :: AgentClient -> UserId -> ConnId -> UserId -> AM ()
905908
changeConnectionUser' c oldUserId connId newUserId = do
@@ -978,12 +981,12 @@ newRcvConnSrv c userId connId enableNtfs cMode userData_ clientData pqInitKeys s
978981
Just ShortLinkCreds {shortLinkId, shortLinkKey}
979982
| qUri == qUri' ->
980983
let link = case cReq of
981-
CRContactUri _ -> CSLContact srv CCTContact shortLinkKey
982-
CRInvitationUri {} -> CSLInvitation srv shortLinkId shortLinkKey
984+
CRContactUri _ -> CSLContact SLSServer CCTContact srv shortLinkKey
985+
CRInvitationUri {} -> CSLInvitation SLSServer srv shortLinkId shortLinkKey
983986
in pure $ CCLink cReq (Just link)
984987
| otherwise -> throwE $ INTERNAL "different rcv queue address"
985988
Nothing ->
986-
let updated (ConnReqUriData _ vr _ _) = (ConnReqUriData SSSimplex vr [qUri] clientData)
989+
let updated (ConnReqUriData _ vr _ _) = (ConnReqUriData SSSimplex vr [qUri'] clientData)
987990
cReq' = case cReq of
988991
CRContactUri crData -> CRContactUri (updated crData)
989992
CRInvitationUri crData e2eParams -> CRInvitationUri (updated crData) e2eParams

0 commit comments

Comments
 (0)