smp protocol: remove creating notifications from NEW command, new ideas for notifications (#1500)

This commit is contained in:
Evgeny
2025-03-30 19:56:16 +01:00
committed by GitHub
parent 04cbed90fb
commit 56bec06856
12 changed files with 216 additions and 95 deletions

View File

@@ -84,7 +84,7 @@ data NtfRequest = NtfRequest NtfPublicAuthKey RcvNtfPublicDhKey
-- LinkId is required with QRContact, to have shorter link - it will be derived from the link_uri. And in this case we do not need to prevent checks that this queue exists.
data QueueReqData = QRMessaging (Maybe QueueLinkData) | QRContact (Maybe (LinkId, QueueLinkData))
-- SenderId should be computed client-side as sha3-256(correlation_id),
-- SenderId should be computed client-side as the first 24 bytes of sha3-384(correlation_id),
-- The server must verify it and reject if it is not.
type QueueLinkData = (SenderId, EncImmutableDataBytes, EncUserDataBytes)
@@ -152,7 +152,7 @@ LGET :: Command Sender
LNK :: SenderId -> QueueLinkData -> BrokerMsg
```
To both include sender_id into the full link before the server response, and to prevent "oracle attack" when a failure to create the queue with the supplied `sender_id` can be used as a proof of queue existense, it is proposed that `sender_id` is computed client-side as `sha3-256(correlation_id)` and validated server-side, where `corelation_id` is the transmission correlation ID.
To both include sender_id into the full link before the server response, and to prevent "oracle attack" when a failure to create the queue with the supplied `sender_id` can be used as a proof of queue existense, it is proposed that `sender_id` is computed client-side as the first 24 bytes of 48 in `sha3-384(correlation_id)` and validated server-side, where `corelation_id` is the transmission correlation ID.
To allow retries and to avoid regenerating all queue data, NEW command must be idempotent, and `correlation_id` must be preserved in command for queue creation, so that the same `correlation_id` and all other data is used in retries. `correlation_id` should be removed after queue creation success.

View File

@@ -0,0 +1,93 @@
# New notifications protocol
## Problem
iOS notifications have these problems:
- iOS notification service crashes exceeding memory limit. This is being addressed by changes in GHC RTS.
- there is a large number of connections, because each member in a group requires individual connection. This will improve with chat relays when each group would require 2-3 connections.
- some notification may be not shown if notification with reply/mention is skipped, and instead some other message is delivered, which may be muted. This would not improve without some changes, as notifications may be skipped anyway.
- client devices delay communication with ntf server because it is done in background, and by that time the app may be suspended.
- notification server represents a bottleneck, as it has to be owned by the app vendor, and the current design when ntf server subscribes to notifications scales very badly.
This RFC is based on the previous [RFC related to notifications](./2024-09-25-ios-notifications-2.md).
## Solution
As notification server has to know client token and currently it associates subscriptions with this token anyway, we are not gaining any privacy and security by using per-subscription keys - both authorization and encryption keys of notification subscription can be dropped.
We still need to store the list of queue IDs associated with the token on the notification server, but we do not need any per-queue keys on the notification server, and we don't need subscriptions - it's effectively a simple set of IDs, with no other information.
In this case, when queue is created the client would supply notifier ID - it has to be derived from correlation ID, to prevent existense check (see previous RFC). As we also supply sender ID, instead of deriving it as sha3-192 of correlation ID, they both can be derived as sha3-384 and split to two IDs - 24 bytes each.
The notification server will maintain a rotating list of server keys with the latest key communicated to the client every time the token is registered and checked. The keys would expire after, say, 1 week or 1 month, and removed from notification server on expiration.
The packet containing association between notifier queue ID and token will be crypto_box encrypted using key agreement between identified notification server master key and an ephemeral per packet (effectively, per-queue) client-key.
Deleting the queue may also include encrypted packet that would verify that the client deleted the queue.
Instead of notification server subscribing to the notifications creating a lot of traffic for the queues without messages, the SMP server would push notifications via NTF server connection (whether via NTF or via SMP protocol). This could be used as a mechanism to migrate existing queues when with the next subscription the notification server would communicate it's address to SMP server and this association would be stored together with the queue.
## Protocol design
Additional/changed SMP commands:
```haskell
-- register notification server
-- should be signed with server key
NSRV :: NtfServerCreds -> Command NtfServer
-- response
NSID :: NtfServerId -> BrokerMsg
-- to communicate which server is responsible for the queue
-- should be signed with queue key
NSUB :: Maybe NtfServerId -> Command Notifier
-- subscribe to notificaions from all queues associated with the server
-- should be signed with server key
-- entity ID - NtfServerId
NSSUB :: Command NtfServer
data NtfServerCreds = NtfServerCreds
{ server :: NtfServer,
-- NTF server certificate chain that should match fingerpring in address
cert :: X.CertificateChain,
-- server autorizatio key to sign server subscription requests
authKey :: X.SignedExact X.PubKey
}
-- entity ID is recipient ID
NSKEY :: NtfSubscription -> Command Recipient
data NtfSubscription = NtfSubscription
-- key to encrypt notifications e2e with the client
{ ntfPubDbKey :: RcvNtfPublicDhKey,
ntfServer :: NtfServer,
-- should be linked to correlation ID to prevent existense check
-- the ID sent to notification server could be its hash?
ntfId :: NotifierId,
encNtfTokenAssoc :: EncDataBytes
}
-- before the encryption - equivalent to NSUB command, but without key to authorize requests to specific queue
data NtfTokenAssoc = NtfTokenAssoc
{ signature :: SignatureEd25519,
tknId :: NtfTokenId,
ntfQueue :: SMPQueueNtf
}
```
SMP server will need to maintain the list of Ntf servers and their credentials, and when NSSUB arrives to make only one subscription. When message arrives it would deliver notification to the correct connection via queue / ntf server association.
Ntf server needs to maintain three indices to the same data:
- `(smpServer, queueId) -> tokenId` - to deliver notification to the correct token
- `tokenId -> [smpServer -> [queueId]]` - to remove all queues when token is removed, and to store/update these associations effficiently - store log may have one compact line per token (after compacting), or per token/server combination.
- `[smpServer]` - array of SMP servers to subscribe to.
## Mention notifications
Currently we are marking messages with T (true) for messages that require notifications and F (false) for messages that don't require. Sender does not know whether the recipient has notifications disabled, enabled or in mentions-only mode.
The proposal is to:
- add additional values to this metadata, e.g. 2 (priority) and 3 (high priority) (and T/F could be sent as 0/1 respectively) - that is, to deliver notifications even if notifications are generally disabled (they can still be further filtered by the client).
- instead of deleting notification credentials when notifications are disabled - which is costly - communicate to SMP server the change of notificaion priority level, e.g. the client could set minimal notification priority to deliver notifications, where 0 would mean disabling it completely, 1 enable for all, 2 for priority 2+, 3 for priority 3. The downside here is that it could be used for timing correlation of queues in the group, but it already can be used on bulk deletions of ntf credentials for these queues and when sending messages.

View File

@@ -908,8 +908,9 @@ newRcvConnSrv c userId connId enableNtfs cMode userData_ clientData pqInitKeys s
createRcvQueue :: Maybe C.CbNonce -> ClntQueueReqData -> C.KeyPairX25519 -> AM (RcvQueue, SMPQueueUri)
createRcvQueue nonce_ qd e2eKeys = do
AgentConfig {smpClientVRange = vr} <- asks config
let ntfCreds_ = Nothing -- TODO [short links]
(rq, qUri, tSess, sessId) <- newRcvQueue_ c userId connId srvWithAuth vr qd subMode ntfCreds_ nonce_ e2eKeys `catchAgentError` \e -> liftIO (print e) >> throwE e
-- TODO [notifications] send correct NTF credentials here
-- let ntfCreds_ = Nothing
(rq, qUri, tSess, sessId) <- newRcvQueue_ c userId connId srvWithAuth vr qd subMode nonce_ e2eKeys `catchAgentError` \e -> liftIO (print e) >> throwE e
atomically $ incSMPServerStat c userId srv connCreated
rq' <- withStore c $ \db -> updateNewConnRcv db connId rq
lift . when (subMode == SMSubscribe) $ addNewQueueSubscription c rq' tSess sessId
@@ -934,7 +935,8 @@ newRcvConnSrv c userId connId enableNtfs cMode userData_ clientData pqInitKeys s
nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g
sigKeys@(_, privSigKey) <- atomically $ C.generateKeyPair @'C.Ed25519 g
AgentConfig {smpClientVRange = vr, smpAgentVRange} <- asks config
let sndId = SMP.EntityId $ C.sha3_256 corrId
-- TODO [notifications] the remaining 24 bytes are reserved for notifier ID
let sndId = SMP.EntityId $ B.take 24 $ C.sha3_384 corrId
sndSecure = case cMode of SCMContact -> False; SCMInvitation -> True
qUri = SMPQueueUri vr $ SMPQueueAddress srv sndId e2eDhKey sndSecure
connReq <- createConnReq qUri
@@ -1117,8 +1119,8 @@ joinConnSrvAsync _c _userId _connId _enableNtfs (CRContactUri _) _cInfo _subMode
createReplyQueue :: AgentClient -> ConnData -> SndQueue -> SubscriptionMode -> SMPServerWithAuth -> AM SMPQueueInfo
createReplyQueue c ConnData {userId, connId, enableNtfs} SndQueue {smpClientVersion} subMode srv = do
-- TODO [short links] ntf credentials
(rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) SCMInvitation subMode Nothing
-- TODO [notifications] send correct NTF credentials here
(rq, qUri, tSess, sessId) <- newRcvQueue c userId connId srv (versionToRange smpClientVersion) SCMInvitation subMode -- Nothing
atomically $ incSMPServerStat c userId (qServer rq) connCreated
let qInfo = toVersionT qUri smpClientVersion
rq' <- withStore c $ \db -> upgradeSndConnToDuplex db connId rq
@@ -1886,8 +1888,8 @@ switchDuplexConnection c (DuplexConnection cData@ConnData {connId, userId} rqs s
-- try to get the server that is different from all queues, or at least from the primary rcv queue
srvAuth@(ProtoServerWithAuth srv _) <- getNextSMPServer c userId $ map qServer (L.toList rqs) <> map qServer (L.toList sqs)
srv' <- if srv == server then getNextSMPServer c userId [server] else pure srvAuth
-- TODO [short links] send correct NTF credentials here
(q, qUri, tSess, sessId) <- newRcvQueue c userId connId srv' clientVRange SCMInvitation SMSubscribe Nothing
-- TODO [notifications] send correct NTF credentials here
(q, qUri, tSess, sessId) <- newRcvQueue c userId connId srv' clientVRange SCMInvitation SMSubscribe -- Nothing
let rq' = (q :: NewRcvQueue) {primary = True, dbReplaceQueueId = Just dbQueueId}
rq'' <- withStore c $ \db -> addConnRcvQueue db connId rq'
lift $ addNewQueueSubscription c rq'' tSess sessId

View File

@@ -267,7 +267,6 @@ import Simplex.Messaging.Protocol
SubscriptionMode (..),
QueueReqData (..),
QueueLinkData,
NewNtfCreds,
UserProtocol,
VersionRangeSMPC,
VersionSMPC,
@@ -1232,7 +1231,8 @@ runSMPServerTest c userId (ProtoServerWithAuth srv auth) = do
(sKey, spKey) <- atomically $ C.generateAuthKeyPair sa g
(dhKey, _) <- atomically $ C.generateKeyPair g
r <- runExceptT $ do
SMP.QIK {rcvId, sndId, queueMode} <- liftError (testErr TSCreateQueue) $ createSMPQueue smp Nothing rKeys dhKey auth SMSubscribe (QRMessaging Nothing) Nothing
-- TODO [notifications]
SMP.QIK {rcvId, sndId, queueMode} <- liftError (testErr TSCreateQueue) $ createSMPQueue smp Nothing rKeys dhKey auth SMSubscribe (QRMessaging Nothing) -- Nothing
liftError (testErr TSSecureQueue) $
case queueMode of
Just QMMessaging -> secureSndSMPQueue smp spKey sndId sKey
@@ -1343,12 +1343,12 @@ getSessionMode :: MonadIO m => AgentClient -> m TransportSessionMode
getSessionMode = fmap sessionMode . getNetworkConfig
{-# INLINE getSessionMode #-}
-- TODO [short links] add ntf credentials to RcvQueue
newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SConnectionMode c -> SubscriptionMode -> Maybe NewNtfCreds -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId)
newRcvQueue c userId connId srv vRange cMode subMode ntfCreds = do
-- TODO [notifications]
newRcvQueue :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> SConnectionMode c -> SubscriptionMode -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId)
newRcvQueue c userId connId srv vRange cMode subMode = do
let qrd = case cMode of SCMInvitation -> CQRMessaging Nothing; SCMContact -> CQRContact Nothing
e2eKeys <- atomically . C.generateKeyPair =<< asks random
newRcvQueue_ c userId connId srv vRange qrd subMode ntfCreds Nothing e2eKeys
newRcvQueue_ c userId connId srv vRange qrd subMode Nothing e2eKeys
data ClntQueueReqData
= CQRMessaging (Maybe (CQRData (SMP.SenderId, QueueLinkData)))
@@ -1365,18 +1365,18 @@ queueReqData = \case
CQRMessaging d -> QRMessaging $ srvReq <$> d
CQRContact d -> QRContact $ srvReq <$> d
newRcvQueue_ :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> ClntQueueReqData -> SubscriptionMode -> Maybe NewNtfCreds -> Maybe C.CbNonce -> C.KeyPairX25519 -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId)
newRcvQueue_ c userId connId (ProtoServerWithAuth srv auth) vRange cqrd subMode ntfCreds nonce_ (e2eDhKey, e2ePrivKey) = do
newRcvQueue_ :: AgentClient -> UserId -> ConnId -> SMPServerWithAuth -> VersionRangeSMPC -> ClntQueueReqData -> SubscriptionMode -> Maybe C.CbNonce -> C.KeyPairX25519 -> AM (NewRcvQueue, SMPQueueUri, SMPTransportSession, SessionId)
newRcvQueue_ c userId connId (ProtoServerWithAuth srv auth) vRange cqrd subMode nonce_ (e2eDhKey, e2ePrivKey) = do
C.AuthAlg a <- asks (rcvAuthAlg . config)
g <- asks random
rKeys@(_, rcvPrivateKey) <- atomically $ C.generateAuthKeyPair a g
(dhKey, privDhKey) <- atomically $ C.generateKeyPair g
logServer "-->" c srv NoEntity "NEW"
tSess <- mkTransportSession c userId srv connId
-- TODO [short links] serverNtfCreds
r@(thParams', QIK {rcvId, sndId, rcvPublicDhKey, queueMode, serverNtfCreds}) <-
-- TODO [notifications]
r@(thParams', QIK {rcvId, sndId, rcvPublicDhKey, queueMode}) <-
withClient c tSess $ \(SMPConnectedClient smp _) ->
(thParams smp,) <$> createSMPQueue smp nonce_ rKeys dhKey auth subMode (queueReqData cqrd) ntfCreds
(thParams smp,) <$> createSMPQueue smp nonce_ rKeys dhKey auth subMode (queueReqData cqrd)
liftIO . logServer "<--" c srv NoEntity $ B.unwords ["IDS", logSecret rcvId, logSecret sndId]
shortLink <- mkShortLinkCreds r
let rq =
@@ -1398,7 +1398,7 @@ newRcvQueue_ c userId connId (ProtoServerWithAuth srv auth) vRange cqrd subMode
dbReplaceQueueId = Nothing,
rcvSwchStatus = Nothing,
smpClientVersion = maxVersion vRange,
clientNtfCreds = Nothing, -- TODO [short links]
clientNtfCreds = Nothing,
deleteErrors = 0
}
qUri = SMPQueueUri vRange $ SMPQueueAddress srv sndId e2eDhKey sndSecure

View File

@@ -719,10 +719,11 @@ createSMPQueue ::
Maybe BasicAuth ->
SubscriptionMode ->
QueueReqData ->
Maybe NewNtfCreds ->
-- TODO [notifications]
-- Maybe NewNtfCreds ->
ExceptT SMPClientError IO QueueIdsKeys
createSMPQueue c nonce_ (rKey, rpKey) dhKey auth subMode qrd ntfCreds =
sendProtocolCommand_ c nonce_ Nothing (Just rpKey) NoEntity (Cmd SRecipient $ NEW $ NewQueueReq rKey dhKey auth subMode (Just qrd) ntfCreds) >>= \case
createSMPQueue c nonce_ (rKey, rpKey) dhKey auth subMode qrd =
sendProtocolCommand_ c nonce_ Nothing (Just rpKey) NoEntity (Cmd SRecipient $ NEW $ NewQueueReq rKey dhKey auth subMode (Just qrd)) >>= \case
IDS qik -> pure qik
r -> throwE $ unexpectedResponse r

View File

@@ -171,6 +171,7 @@ module Simplex.Messaging.Crypto
sha256Hash,
sha512Hash,
sha3_256,
sha3_384,
-- * Message padding / un-padding
canPad,
@@ -211,7 +212,7 @@ import Crypto.Cipher.AES (AES256)
import qualified Crypto.Cipher.Types as AES
import qualified Crypto.Cipher.XSalsa as XSalsa
import qualified Crypto.Error as CE
import Crypto.Hash (Digest, SHA256 (..), SHA3_256, SHA512 (..), hash, hashDigestSize)
import Crypto.Hash (Digest, SHA3_256, SHA3_384, SHA256 (..), SHA512 (..), hash, hashDigestSize)
import qualified Crypto.KDF.HKDF as H
import qualified Crypto.MAC.Poly1305 as Poly1305
import qualified Crypto.PubKey.Curve25519 as X25519
@@ -978,6 +979,11 @@ sha3_256 :: ByteString -> ByteString
sha3_256 = BA.convert . (hash :: ByteString -> Digest SHA3_256)
{-# INLINE sha3_256 #-}
-- | SHA3-384 digest.
sha3_384 :: ByteString -> ByteString
sha3_384 = BA.convert . (hash :: ByteString -> Digest SHA3_384)
{-# INLINE sha3_384 #-}
-- | AEAD-GCM encryption with associated data.
--
-- Used as part of double ratchet encryption.

View File

@@ -65,7 +65,6 @@ module Simplex.Messaging.Protocol
EncFixedDataBytes,
EncUserDataBytes,
EncDataBytes (..),
NewNtfCreds (..),
Party (..),
Cmd (..),
DirectParty,
@@ -73,7 +72,6 @@ module Simplex.Messaging.Protocol
SParty (..),
PartyI (..),
QueueIdsKeys (..),
ServerNtfCreds (..),
ProtocolErrorType (..),
ErrorType (..),
CommandError (..),
@@ -457,8 +455,9 @@ data NewQueueReq = NewQueueReq
rcvDhKey :: RcvPublicDhKey,
auth_ :: Maybe BasicAuth,
subMode :: SubscriptionMode,
queueReqData :: Maybe QueueReqData,
ntfCreds :: Maybe NewNtfCreds
queueReqData :: Maybe QueueReqData
-- TODO [notifications]
-- ntfCreds :: Maybe NewNtfCreds
}
deriving (Show)
@@ -502,7 +501,8 @@ instance ToField EncDataBytes where
toField (EncDataBytes s) = toField (Binary s)
{-# INLINE toField #-}
data NewNtfCreds = NewNtfCreds NtfPublicAuthKey RcvNtfPublicDhKey deriving (Show)
-- TODO [notifications]
-- data NewNtfCreds = NewNtfCreds NtfPublicAuthKey RcvNtfPublicDhKey deriving (Show)
instance StrEncoding SubscriptionMode where
strEncode = \case
@@ -533,9 +533,10 @@ instance Encoding QueueReqData where
'C' -> QRContact <$> smpP
_ -> fail "bad QueueReqData"
instance Encoding NewNtfCreds where
smpEncode (NewNtfCreds authKey dhKey) = smpEncode (authKey, dhKey)
smpP = NewNtfCreds <$> smpP <*> smpP
-- TODO [notifications]
-- instance Encoding NewNtfCreds where
-- smpEncode (NewNtfCreds authKey dhKey) = smpEncode (authKey, dhKey)
-- smpP = NewNtfCreds <$> smpP <*> smpP
type SenderCanSecure = Bool
@@ -1243,17 +1244,19 @@ data QueueIdsKeys = QIK
sndId :: SenderId,
rcvPublicDhKey :: RcvPublicDhKey,
queueMode :: Maybe QueueMode, -- TODO remove Maybe when min version is 9 (sndAuthKeySMPVersion)
linkId :: Maybe LinkId,
serverNtfCreds :: Maybe ServerNtfCreds
linkId :: Maybe LinkId
-- TODO [notifications]
-- serverNtfCreds :: Maybe ServerNtfCreds
}
deriving (Eq, Show)
data ServerNtfCreds = ServerNtfCreds NotifierId RcvNtfPublicDhKey
deriving (Eq, Show)
-- TODO [notifications]
-- data ServerNtfCreds = ServerNtfCreds NotifierId RcvNtfPublicDhKey
-- deriving (Eq, Show)
instance Encoding ServerNtfCreds where
smpEncode (ServerNtfCreds nId dhKey) = smpEncode (nId, dhKey)
smpP = ServerNtfCreds <$> smpP <*> smpP
-- instance Encoding ServerNtfCreds where
-- smpEncode (ServerNtfCreds nId dhKey) = smpEncode (nId, dhKey)
-- smpP = ServerNtfCreds <$> smpP <*> smpP
-- | Recipient's private key used by the recipient to authorize (v6: sign, v7: encrypt hash) SMP commands.
--
@@ -1481,8 +1484,8 @@ class ProtocolMsgTag (Tag msg) => ProtocolEncoding v err msg | msg -> err, msg -
instance PartyI p => ProtocolEncoding SMPVersion ErrorType (Command p) where
type Tag (Command p) = CommandTag p
encodeProtocol v = \case
NEW NewQueueReq {rcvAuthKey = rKey, rcvDhKey = dhKey, auth_, subMode, queueReqData, ntfCreds}
| v >= shortLinksSMPVersion -> new <> e (auth_, subMode, queueReqData, ntfCreds)
NEW NewQueueReq {rcvAuthKey = rKey, rcvDhKey = dhKey, auth_, subMode, queueReqData}
| v >= shortLinksSMPVersion -> new <> e (auth_, subMode, queueReqData)
| v >= sndAuthKeySMPVersion -> new <> e (auth_, subMode, senderCanSecure (queueReqMode <$> queueReqData))
| otherwise -> new <> auth <> e subMode
where
@@ -1556,18 +1559,19 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where
CT SRecipient tag ->
Cmd SRecipient <$> case tag of
NEW_
| v >= shortLinksSMPVersion -> NEW <$> new smpP smpP smpP
| v >= sndAuthKeySMPVersion -> NEW <$> new smpP (qReq <$> smpP) (pure Nothing)
| otherwise -> NEW <$> new auth (pure Nothing) (pure Nothing)
| v >= shortLinksSMPVersion -> NEW <$> new smpP smpP
| v >= sndAuthKeySMPVersion -> NEW <$> new smpP (qReq <$> smpP)
| otherwise -> NEW <$> new auth (pure Nothing)
where
new p1 p2 p3 = do
new p1 p2 = do
rcvAuthKey <- _smpP
rcvDhKey <- smpP
auth_ <- p1
subMode <- smpP
queueReqData <- p2
ntfCreds <- p3
pure NewQueueReq {rcvAuthKey, rcvDhKey, auth_, subMode, queueReqData, ntfCreds}
-- TODO [notifications]
-- ntfCreds <- p3
pure NewQueueReq {rcvAuthKey, rcvDhKey, auth_, subMode, queueReqData} -- ntfCreds
auth = optional (A.char 'A' *> smpP)
qReq sndSecure = Just $ if sndSecure then QRMessaging Nothing else QRContact Nothing
SUB_ -> pure SUB
@@ -1605,8 +1609,8 @@ instance ProtocolEncoding SMPVersion ErrorType Cmd where
instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
type Tag BrokerMsg = BrokerMsgTag
encodeProtocol v = \case
IDS QIK {rcvId, sndId, rcvPublicDhKey = srvDh, queueMode, linkId, serverNtfCreds}
| v >= shortLinksSMPVersion -> ids <> e queueMode <> e linkId <> e serverNtfCreds
IDS QIK {rcvId, sndId, rcvPublicDhKey = srvDh, queueMode, linkId}
| v >= shortLinksSMPVersion -> ids <> e queueMode <> e linkId
| v >= sndAuthKeySMPVersion -> ids <> e (senderCanSecure queueMode)
| otherwise -> ids
where
@@ -1640,13 +1644,21 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where
where
bodyP = EncRcvMsgBody . unTail <$> smpP
IDS_
| v >= shortLinksSMPVersion -> ids smpP smpP smpP
| v >= sndAuthKeySMPVersion -> ids (qm <$> smpP) nothing nothing
| otherwise -> ids nothing nothing nothing
| v >= shortLinksSMPVersion -> ids smpP smpP
| v >= sndAuthKeySMPVersion -> ids (qm <$> smpP) nothing
| otherwise -> ids nothing nothing
where
qm sndSecure = Just $ if sndSecure then QMMessaging else QMContact
nothing = pure Nothing
ids p1 p2 p3 = IDS <$> (QIK <$> _smpP <*> smpP <*> smpP <*> p1 <*> p2 <*> p3)
ids p1 p2 = do
rcvId <- _smpP
sndId <- smpP
rcvPublicDhKey <- smpP
queueMode <- p1
linkId <- p2
-- TODO [notifications]
-- serverNtfCreds <- p3
pure $ IDS QIK {rcvId, sndId, rcvPublicDhKey, queueMode, linkId}
LNK_ -> LNK <$> _smpP <*> smpP
NID_ -> NID <$> _smpP <*> smpP
NMSG_ -> NMSG <$> _smpP <*> smpP

View File

@@ -1278,15 +1278,18 @@ client
QUE -> withQueue $ \q qr -> (corrId,entId,) <$> getQueueInfo q qr
where
createQueue :: NewQueueReq -> M (Transmission BrokerMsg)
createQueue NewQueueReq {rcvAuthKey, rcvDhKey, subMode, queueReqData, ntfCreds} = time "NEW" $ do
createQueue NewQueueReq {rcvAuthKey, rcvDhKey, subMode, queueReqData} = time "NEW" $ do
g <- asks random
idSize <- asks $ queueIdBytes . config
updatedAt <- Just <$> liftIO getSystemDate
(rcvPublicDhKey, privDhKey) <- atomically $ C.generateKeyPair g
ntfKeys_ <- forM ntfCreds $ \(NewNtfCreds notifierKey dhKey) -> do
(ntfPubDhKey, ntfPrivDhKey) <- atomically $ C.generateKeyPair g
pure (notifierKey, C.dh' dhKey ntfPrivDhKey, ntfPubDhKey)
-- TODO [notifications]
-- ntfKeys_ <- forM ntfCreds $ \(NewNtfCreds notifierKey dhKey) -> do
-- (ntfPubDhKey, ntfPrivDhKey) <- atomically $ C.generateKeyPair g
-- pure (notifierKey, C.dh' dhKey ntfPrivDhKey, ntfPubDhKey)
let randId = EntityId <$> atomically (C.randomBytes idSize g)
-- TODO [notifications] the remaining 24 bytes are reserver for notifier ID
sndId' = B.take 24 $ C.sha3_384 (bs corrId)
tryCreate 0 = pure $ ERR INTERNAL
tryCreate n = do
(sndId, clntIds, queueData) <- case queueReqData of
@@ -1296,13 +1299,14 @@ client
-- The condition that client-provided sender ID must match hash of correlation ID
-- prevents "ID oracle" attack, when creating queue with supplied ID can be used to check
-- if queue with this ID still exists.
if clntIds && unEntityId sndId /= C.sha3_256 (bs corrId)
if clntIds && unEntityId sndId /= sndId'
then pure $ ERR $ CMD PROHIBITED
else do
rcvId <- randId
ntf <- forM ntfKeys_ $ \(notifierKey, rcvNtfDhSecret, rcvPubDhKey) -> do
notifierId <- randId
pure (NtfCreds {notifierId, notifierKey, rcvNtfDhSecret}, ServerNtfCreds notifierId rcvPubDhKey)
-- TODO [notifications]
-- ntf <- forM ntfKeys_ $ \(notifierKey, rcvNtfDhSecret, rcvPubDhKey) -> do
-- notifierId <- randId
-- pure (NtfCreds {notifierId, notifierKey, rcvNtfDhSecret}, ServerNtfCreds notifierId rcvPubDhKey)
let queueMode = queueReqMode <$> queueReqData
qr =
QueueRec
@@ -1312,7 +1316,8 @@ client
senderKey = Nothing,
queueMode,
queueData,
notifier = fst <$> ntf,
-- TODO [notifications]
notifier = Nothing, -- fst <$> ntf,
status = EntityActive,
updatedAt
}
@@ -1325,11 +1330,12 @@ client
stats <- asks serverStats
incStat $ qCreated stats
incStat $ qCount stats
when (isJust ntf) $ incStat $ ntfCreated stats
-- TODO [notifications]
-- when (isJust ntf) $ incStat $ ntfCreated stats
case subMode of
SMOnlyCreate -> pure ()
SMSubscribe -> void $ subscribeQueue q qr
pure $ IDS QIK {rcvId, sndId, rcvPublicDhKey, queueMode, linkId = fst <$> queueData, serverNtfCreds = snd <$> ntf}
pure $ IDS QIK {rcvId, sndId, rcvPublicDhKey, queueMode, linkId = fst <$> queueData} -- , serverNtfCreds = snd <$> ntf
(corrId,entId,) <$> tryCreate (3 :: Int)
checkMode :: QueueMode -> QueueRec -> M (Either ErrorType BrokerMsg) -> M (Transmission BrokerMsg)

View File

@@ -148,7 +148,7 @@ smpBlockSize = 16384
-- 11 - additional encryption of transport blocks with forward secrecy (10/06/2024)
-- 12 - BLOCKED error for blocked queues (1/11/2025)
-- 14 - proxyServer handshake property to disable transport encryption between server and proxy (1/19/2025)
-- 15 - short links, include creating NTF credentials in NEW command
-- 15 - short links, with associated data passed in NEW of LSET command (3/30/2025)
data SMPVersion

View File

@@ -50,7 +50,7 @@ ntfServerTests :: ATransport -> Spec
ntfServerTests t = do
describe "Notifications server protocol syntax" $ ntfSyntaxTests t
describe "Notification subscriptions (NKEY)" $ testNotificationSubscription t createNtfQueueNKEY
describe "Notification subscriptions (NEW with ntf creds)" $ testNotificationSubscription t createNtfQueueNEW
-- describe "Notification subscriptions (NEW with ntf creds)" $ testNotificationSubscription t createNtfQueueNEW
ntfSyntaxTests :: ATransport -> Spec
ntfSyntaxTests (ATransport t) = do
@@ -185,17 +185,18 @@ createNtfQueueNKEY h sPub nPub = do
let rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey
pure ((sId, rId, rKey, rcvDhSecret), nId, rcvNtfDhSecret)
createNtfQueueNEW :: CreateQueueFunc
createNtfQueueNEW h sPub nPub = do
g <- C.newRandom
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
(dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
(rcvNtfPubDhKey, rcvNtfPrivDhKey) <- atomically $ C.generateKeyPair g
let cmd = NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRMessaging Nothing)) (Just (NewNtfCreds nPub rcvNtfPubDhKey)))
Resp "abcd" NoEntity (IDS (QIK rId sId srvDh _sndSecure _linkId (Just (ServerNtfCreds nId rcvNtfSrvPubDhKey)))) <-
signSendRecv h rKey ("abcd", NoEntity, cmd)
let dhShared = C.dh' srvDh dhPriv
Resp "dabc" rId' OK <- signSendRecv h rKey ("dabc", rId, KEY sPub)
(rId', rId) #== "same queue ID"
let rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey
pure ((sId, rId, rKey, dhShared), nId, rcvNtfDhSecret)
-- TODO [notifications]
-- createNtfQueueNEW :: CreateQueueFunc
-- createNtfQueueNEW h sPub nPub = do
-- g <- C.newRandom
-- (rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
-- (dhPub, dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
-- (rcvNtfPubDhKey, rcvNtfPrivDhKey) <- atomically $ C.generateKeyPair g
-- let cmd = NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRMessaging Nothing)) (Just (NewNtfCreds nPub rcvNtfPubDhKey)))
-- Resp "abcd" NoEntity (IDS (QIK rId sId srvDh _sndSecure _linkId (Just (ServerNtfCreds nId rcvNtfSrvPubDhKey)))) <-
-- signSendRecv h rKey ("abcd", NoEntity, cmd)
-- let dhShared = C.dh' srvDh dhPriv
-- Resp "dabc" rId' OK <- signSendRecv h rKey ("dabc", rId, KEY sPub)
-- (rId', rId) #== "same queue ID"
-- let rcvNtfDhSecret = C.dh' rcvNtfSrvPubDhKey rcvNtfPrivDhKey
-- pure ((sId, rId, rKey, dhShared), nId, rcvNtfDhSecret)

View File

@@ -178,7 +178,7 @@ deliverMessagesViaProxy proxyServ relayServ alg unsecuredMsgs securedMsgs = do
-- prepare receiving queue
(rPub, rPriv) <- atomically $ C.generateAuthKeyPair alg g
(rdhPub, rdhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
SMP.QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc Nothing (rPub, rPriv) rdhPub (Just "correct") SMSubscribe (QRMessaging Nothing) Nothing
SMP.QIK {rcvId, sndId, rcvPublicDhKey = srvDh} <- runExceptT' $ createSMPQueue rc Nothing (rPub, rPriv) rdhPub (Just "correct") SMSubscribe (QRMessaging Nothing)
let dec = decryptMsgV3 $ C.dh' srvDh rdhPriv
-- get proxy session
sess0 <- runExceptT' $ connectSMPProxiedRelay pc relayServ (Just "correct")

View File

@@ -88,10 +88,10 @@ pattern Resp :: CorrId -> QueueId -> BrokerMsg -> SignedTransmission ErrorType B
pattern Resp corrId queueId command <- (_, _, (corrId, queueId, Right command))
pattern New :: RcvPublicAuthKey -> RcvPublicDhKey -> Command 'Recipient
pattern New rPub dhPub = NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRMessaging Nothing)) Nothing)
pattern New rPub dhPub = NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRMessaging Nothing)))
pattern Ids :: RecipientId -> SenderId -> RcvPublicDhKey -> BrokerMsg
pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh _sndSecure _linkId _srvNtfCreds)
pattern Ids rId sId srvDh <- IDS (QIK rId sId srvDh _sndSecure _linkId)
pattern Msg :: MsgId -> MsgBody -> BrokerMsg
pattern Msg msgId body <- MSG RcvMessage {msgId, msgBody = EncRcvMsgBody body}
@@ -264,7 +264,7 @@ testSndSecureProhibited =
g <- C.newRandom
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
(dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
Resp "abcd" rId1 (Ids _rId sId _srvDh) <- signSendRecv r rKey ("abcd", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRContact Nothing)) Nothing))
Resp "abcd" rId1 (Ids _rId sId _srvDh) <- signSendRecv r rKey ("abcd", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just (QRContact Nothing))))
(rId1, NoEntity) #== "creates queue"
(sPub, sKey) <- atomically $ C.generateAuthKeyPair C.SEd448 g
@@ -1064,14 +1064,14 @@ testInvQueueLinkData =
(rPub, rKey) <- atomically $ C.generateAuthKeyPair C.SEd25519 g
(dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
C.CbNonce corrId <- atomically $ C.randomCbNonce g
let sId = EntityId $ C.sha3_256 corrId
let sId = EntityId $ B.take 24 $ C.sha3_384 corrId
ld = (EncDataBytes "fixed data", EncDataBytes "user data")
qrd = QRMessaging $ Just (sId, ld)
-- sender ID must be derived from corrId
Resp "1" NoEntity (ERR (CMD PROHIBITED)) <-
signSendRecv r rKey ("1", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd) Nothing))
Resp corrId' NoEntity (IDS (QIK rId sId' _srvDh (Just QMMessaging) (Just lnkId) Nothing)) <-
signSendRecv r rKey (corrId, NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd) Nothing))
signSendRecv r rKey ("1", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd)))
Resp corrId' NoEntity (IDS (QIK rId sId' _srvDh (Just QMMessaging) (Just lnkId))) <-
signSendRecv r rKey (corrId, NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd)))
(sId', sId) #== "should return the same sender ID"
corrId' `shouldBe` CorrId corrId
-- can't read link data with LGET
@@ -1118,14 +1118,14 @@ testContactQueueLinkData =
(dhPub, _dhPriv :: C.PrivateKeyX25519) <- atomically $ C.generateKeyPair g
C.CbNonce corrId <- atomically $ C.randomCbNonce g
lnkId <- EntityId <$> atomically (C.randomBytes 24 g)
let sId = EntityId $ C.sha3_256 corrId
let sId = EntityId $ B.take 24 $ C.sha3_384 corrId
ld = (EncDataBytes "fixed data", EncDataBytes "user data")
qrd = QRContact $ Just (lnkId, (sId, ld))
-- sender ID must be derived from corrId
Resp "1" NoEntity (ERR (CMD PROHIBITED)) <-
signSendRecv r rKey ("1", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd) Nothing))
Resp corrId' NoEntity (IDS (QIK rId sId' _srvDh (Just QMContact) (Just lnkId') Nothing)) <-
signSendRecv r rKey (corrId, NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd) Nothing))
signSendRecv r rKey ("1", NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd)))
Resp corrId' NoEntity (IDS (QIK rId sId' _srvDh (Just QMContact) (Just lnkId'))) <-
signSendRecv r rKey (corrId, NoEntity, NEW (NewQueueReq rPub dhPub Nothing SMSubscribe (Just qrd)))
(lnkId', lnkId) #== "should return the same link ID"
(sId', sId) #== "should return the same sender ID"
corrId' `shouldBe` CorrId corrId
@@ -1194,8 +1194,8 @@ serverSyntaxTests (ATransport t) = do
describe "NEW" $ do
it "no parameters" $ (sampleSig, "bcda", "", NEW_) >#> ("", "bcda", "", ERR $ CMD SYNTAX)
it "many parameters" $ (sampleSig, "cdab", "", (NEW_, ' ', ('\x01', 'A'), samplePubKey, sampleDhPubKey)) >#> ("", "cdab", "", ERR $ CMD SYNTAX)
it "no signature" $ ("", "dabc", "", ((NEW_, ' ', samplePubKey, sampleDhPubKey), ('0', SMSubscribe, Just (QRMessaging Nothing), '0'))) >#> ("", "dabc", "", ERR $ CMD NO_AUTH)
it "queue ID" $ (sampleSig, "abcd", "12345678", ((NEW_, ' ', samplePubKey, sampleDhPubKey), ('0', SMSubscribe, Just (QRMessaging Nothing), '0'))) >#> ("", "abcd", "12345678", ERR $ CMD HAS_AUTH)
it "no signature" $ ("", "dabc", "", ((NEW_, ' ', samplePubKey, sampleDhPubKey), ('0', SMSubscribe, Just (QRMessaging Nothing)))) >#> ("", "dabc", "", ERR $ CMD NO_AUTH)
it "queue ID" $ (sampleSig, "abcd", "12345678", ((NEW_, ' ', samplePubKey, sampleDhPubKey), ('0', SMSubscribe, Just (QRMessaging Nothing)))) >#> ("", "abcd", "12345678", ERR $ CMD HAS_AUTH)
describe "KEY" $ do
it "valid syntax" $ (sampleSig, "bcda", "12345678", (KEY_, ' ', samplePubKey)) >#> ("", "bcda", "12345678", ERR AUTH)
it "no parameters" $ (sampleSig, "cdab", "12345678", KEY_) >#> ("", "cdab", "12345678", ERR $ CMD SYNTAX)