Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ jobs:
- name: Set up Docker Buildx
uses: simplex-chat/docker-setup-buildx-action@v3

- name: Install PostgreSQL 15 client tools
if: matrix.os == '22.04'
shell: bash
run: |
# Import the repository signing key
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc
# Add the PostgreSQL APT repository
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'
# Update repository and install postgresql tools
sudo apt update
sudo apt -y install postgresql-client-15

- name: Build and cache Docker image
uses: simplex-chat/docker-build-push-action@v6
with:
Expand Down Expand Up @@ -82,7 +95,7 @@ jobs:
build/${{ matrix.platform_name }}:latest

- name: Build smp-server (postgresql) and tests
shell: docker exec -t builder sh {0}
shell: docker exec -t builder sh -eu {0}
run: |
cabal update
cabal build --jobs=$(nproc) --enable-tests -fserver_postgres
Expand All @@ -106,7 +119,7 @@ jobs:
docker cp builder:/out/smp-server ./smp-server-postgres-ubuntu-${{ matrix.platform_name }}

- name: Build everything else (standard)
shell: docker exec -t builder sh {0}
shell: docker exec -t builder sh -eu {0}
run: |
cabal build --jobs=$(nproc)
mkdir -p /out
Expand Down
1 change: 1 addition & 0 deletions apps/smp-server/static/a/index.html
1 change: 1 addition & 0 deletions apps/smp-server/static/c/index.html
1 change: 1 addition & 0 deletions apps/smp-server/static/i/index.html
13 changes: 9 additions & 4 deletions apps/smp-server/web/Static.hs
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,16 @@ generateSite si onionHost sitePath = do
B.writeFile (sitePath </> "index.html") $ serverInformation si onionHost
createDirectoryIfMissing True $ sitePath </> "media"
forM_ E.mediaContent $ \(path, bs) -> B.writeFile (sitePath </> "media" </> path) bs
createDirectoryIfMissing True $ sitePath </> "contact"
B.writeFile (sitePath </> "contact" </> "index.html") E.linkHtml
createDirectoryIfMissing True $ sitePath </> "invitation"
B.writeFile (sitePath </> "invitation" </> "index.html") E.linkHtml
createLinkPage "contact"
createLinkPage "invitation"
createLinkPage "a"
createLinkPage "c"
createLinkPage "i"
logInfo $ "Generated static site contents at " <> tshow sitePath
where
createLinkPage path = do
createDirectoryIfMissing True $ sitePath </> path
B.writeFile (sitePath </> path </> "index.html") E.linkHtml

serverInformation :: ServerInformation -> Maybe TransportHost -> ByteString
serverInformation ServerInformation {config, information} onionHost = render E.indexHtml substs
Expand Down
108 changes: 82 additions & 26 deletions rfcs/2025-04-04-short-links-for-groups.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,45 +59,101 @@ While the server domain would be used as the hostname in group link, it may cont
Pros: separates additional complexity to where it is needed, allowing reliability and redundancy for group ownership.
Cons: complexity, coupling between SMP and chat protocol.

## Design ideas for group as a separate entity type
## Design for channel/group as a separate queue mode

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

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).
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.

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.
Pros:
- allows chat relays to send messages to all owners (e.g., channel can be secured with the list of snd keys, one per relay).
- quite easy to evolve from the current design.
- extensible.
Cons:
- close to "solution in search of a problem".
- 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.
Comment thread
epoberezkin marked this conversation as resolved.

Option 2.

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.

Pros:
- simpler design.
- we could allow sending messages to it too, with the "main" owner receiving them. This could be negotiated in the protocol.
- 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).
- 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.
Cons:
- if no messages are accepted, this is not even a queue.
- 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).

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.

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.

## Multiple owners managing queue data.

Option 1: Use the same keys in SMP as when signing queue data.

Option 2: Use different keys.

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.

We still need to bind the mutable data updates to the "genesis" signature key (the one included in the immutable data).

The proposed design:

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.
- when mutable data is signed by genesis key, then it is bound, and no changes is needed.
Comment thread
epoberezkin marked this conversation as resolved.
- 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.

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

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.
```haskell
data UserLinkData = UserLinkData
{ agentVRange :: VersionRangeSMPA,
userData :: ConnInfo
}
```

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?
Proposed mutable data:

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?
```haskell
data UserLinkData = UserLinkData
{ agentVRange :: VersionRangeSMPA,
owners :: [OwnerInfo]
userData :: ConnInfo
}

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:
type OwnerId = ByteString

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

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).
The size of the OwnerInfo record encoding is:
- ownerId: 1 + 12
- ownerKey: 1 + 32
- ownerSig: 1 + 64
- ownerAuthId: 1 + 12
- ownerAuthSig: 1 + 64

The additional commands required would be to add, get and delete link owners:
- 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?
- remove owner (ODEL): remove owner from link by owner ID (both before and after new owner accepted ownership).
- get owners (OGET): get current owner IDs and their public keys.
- 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?
~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.

While initially we don't need to build support for multisig in UX, it can be easily added later with this design.
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).

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.
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.

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:
- 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.
- 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).
- 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.
When additional owners want to be added to the group, they would have to provide any of the current owners:
- 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.
- the signature of the immutable data by their member key included in their profile.
Comment thread
epoberezkin marked this conversation as resolved.
- 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.

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.
Potentially, there could be one command to update keys and link data, so that they are consistent.
9 changes: 7 additions & 2 deletions simplexmq.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,9 @@ test-suite simplexmq-test
other-modules:
AgentTests.SchemaDump
AgentTests.SQLiteTests
if flag(server_postgres)
other-modules:
ServerTests.SchemaDump
hs-source-dirs:
tests
apps/smp-server/web
Expand Down Expand Up @@ -537,11 +540,13 @@ test-suite simplexmq-test
if flag(client_postgres)
cpp-options: -DdbPostgres
else
build-depends:
memory
, sqlcipher-simple
if !flag(client_postgres) || flag(server_postgres)
build-depends:
deepseq ==1.4.*
, memory
, process
, sqlcipher-simple
if flag(client_postgres) || flag(server_postgres)
build-depends:
postgresql-simple ==0.7.*
Expand Down
25 changes: 14 additions & 11 deletions src/Simplex/Messaging/Agent.hs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ import Simplex.Messaging.Protocol
MsgFlags (..),
NtfServer,
ProtoServerWithAuth (..),
ProtocolServer (..),
ProtocolType (..),
ProtocolTypeI (..),
QueueLinkData,
Expand Down Expand Up @@ -379,7 +380,7 @@ deleteContactShortLink c = withAgentEnv c . deleteContactShortLink' c
{-# INLINE deleteContactShortLink #-}

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

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

-- TODO [short links] remove 1-time invitation data and link ID from the server after the message is sent.
getConnShortLink' :: forall c. AgentClient -> UserId -> ConnShortLink c -> AM (ConnectionRequestUri c, ConnInfo)
getConnShortLink' :: forall c. AgentClient -> UserId -> ConnShortLink c -> AM (ConnectionRequestUri c, ConnLinkData c)
getConnShortLink' c userId = \case
CSLInvitation srv linkId linkKey -> do
CSLInvitation _ srv linkId linkKey -> do
g <- asks random
invLink <- withStore' c $ \db -> do
getInvShortLink db srv linkId >>= \case
Expand All @@ -886,20 +887,22 @@ getConnShortLink' c userId = \case
ld@(sndId, _) <- secureGetQueueLink c userId invLink
withStore' c $ \db -> setInvShortLinkSndId db invLink sndId
decryptData srv linkKey k ld
CSLContact srv _ linkKey -> do
CSLContact _ _ srv linkKey -> do
let (linkId, k) = SL.contactShortLinkKdf linkKey
ld <- getQueueLink c userId srv linkId
decryptData srv linkKey k ld
where
decryptData :: ConnectionModeI c => SMPServer -> LinkKey -> C.SbKey -> (SMP.SenderId, QueueLinkData) -> AM (ConnectionRequestUri c, ConnInfo)
decryptData :: ConnectionModeI c => SMPServer -> LinkKey -> C.SbKey -> (SMP.SenderId, QueueLinkData) -> AM (ConnectionRequestUri c, ConnLinkData c)
decryptData srv linkKey k (sndId, d) = do
r@(cReq, _) <- liftEither $ SL.decryptLinkData @c linkKey k d
unless ((srv, sndId) `sameQAddress` qAddress (connReqQueue cReq)) $
let (srv', sndId') = qAddress (connReqQueue cReq)
unless (srv `sameSrvHost` srv' && sndId == sndId') $
throwE $ AGENT $ A_LINK "different address"
pure r
sameSrvHost ProtocolServer {host = h :| _} ProtocolServer {host = hs} = h `elem` hs

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

changeConnectionUser' :: AgentClient -> UserId -> ConnId -> UserId -> AM ()
changeConnectionUser' c oldUserId connId newUserId = do
Expand Down Expand Up @@ -978,12 +981,12 @@ newRcvConnSrv c userId connId enableNtfs cMode userData_ clientData pqInitKeys s
Just ShortLinkCreds {shortLinkId, shortLinkKey}
| qUri == qUri' ->
let link = case cReq of
CRContactUri _ -> CSLContact srv CCTContact shortLinkKey
CRInvitationUri {} -> CSLInvitation srv shortLinkId shortLinkKey
CRContactUri _ -> CSLContact SLSServer CCTContact srv shortLinkKey
CRInvitationUri {} -> CSLInvitation SLSServer srv shortLinkId shortLinkKey
in pure $ CCLink cReq (Just link)
| otherwise -> throwE $ INTERNAL "different rcv queue address"
Nothing ->
let updated (ConnReqUriData _ vr _ _) = (ConnReqUriData SSSimplex vr [qUri] clientData)
let updated (ConnReqUriData _ vr _ _) = (ConnReqUriData SSSimplex vr [qUri'] clientData)
cReq' = case cReq of
CRContactUri crData -> CRContactUri (updated crData)
CRInvitationUri crData e2eParams -> CRInvitationUri (updated crData) e2eParams
Expand Down
Loading
Loading