Skip to content

Commit

Permalink
agent: attempt at faster queue rotation (does not work)
Browse files Browse the repository at this point in the history
  • Loading branch information
epoberezkin committed Jun 30, 2024
1 parent a99ce61 commit b48b86f
Show file tree
Hide file tree
Showing 6 changed files with 98 additions and 81 deletions.
63 changes: 0 additions & 63 deletions protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd

This file was deleted.

4 changes: 2 additions & 2 deletions protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ sequenceDiagram
A ->> S: SEND: QADD (R'): send address<br>of the new queue(s)
S ->> B: MSG: QADD (R')
B ->> R': SKEY: secure new queue
B ->> R': SEND: QTEST
R' ->> A: MSG: QTEST
B ->> R': SEND: QSEC: to agree shared secret
R' ->> A: MSG: QSEC
A ->> R: DEL: delete the old queue
B ->> R': SEND: send messages to the new queue
R' ->> A: MSG: receive messages from the new queue
Expand Down
8 changes: 7 additions & 1 deletion rfcs/2024-06-14-fast-connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,18 @@ These are the proposed changes:
5. Accepting client will secure the messaging queue before sending the confirmation to it.
6. Initiating client will secure the messaging queue before sending the confirmation.

See [this sequence diagram](../protocol/diagrams/duplex-messaging/duplex-creating-v6.mmd) for the updated handshake protocol.
See [this sequence diagram](../protocol/diagrams/duplex-messaging/duplex-creating-fast.mmd) for the updated handshake protocol.

Changes to threat model: the attacker who compromised TLS and knows the queue address can block the connection, as the protocol no longer requires the recipient to decrypt the confirmation to secure the queue.

Possibly, "fast connection" should be an option in Privacy & security settings.

## Queue rotation

It is possible to design a faster connection rotation protocol that also uses only 2 instead of 4 messages, QADD and SMP confirmation (to agree per-queue encryption) - it would require to stop delivery to the old queue as soon as QSEC message is sent, without any additional test messages.

It would also require sending a new message envelope with the DH key in the public header instead of the usual confirmation message or a normal message.

## Implementation questions

Currently we store received confirmations in the database, so that the client can confirm them. This becomes unnecessary.
78 changes: 63 additions & 15 deletions src/Simplex/Messaging/Agent.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1194,6 +1194,25 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do
notify . SWITCH QDRcv SPSecured $ connectionStats conn'
_ -> internalErr "ICQSecure: no switching queue found"
_ -> internalErr "ICQSecure: queue address not found in connection"
ICQSndSecure sId ->
withServer $ \srv -> tryWithLock "ICQSndSecure" . withDuplexConn $ \(DuplexConnection cData rqs sqs) ->
case find (sameQueue (srv, sId)) sqs of
Just sq'@SndQueue {server, sndId, sndSecure, status, smpClientVersion, e2ePubKey = Just dhPublicKey, dbReplaceQueueId = Just replaceQId} ->
case find ((replaceQId ==) . dbQId) sqs of
Just sq1 -> when (status == New) $ do
secureSndQueue c sq'
withStore' c $ \db -> setSndQueueStatus db sq' Secured
let sq'' = (sq' :: SndQueue) {status = Secured}
queueAddress = SMPQueueAddress {smpServer = server, senderId = sndId, dhPublicKey, sndSecure}
qInfo = SMPQueueInfo {clientVersion = smpClientVersion, queueAddress}
-- sending QSEC to the new queue only, the old one will be removed if sent successfully
void . enqueueMessages c cData [sq''] SMP.noMsgFlags $ QSEC [qInfo]
sq1' <- withStore' c $ \db -> setSndSwitchStatus db sq1 $ Just SSSendingQSEC
let sqs' = updatedQs sq1' sqs
conn' = DuplexConnection cData rqs sqs'
notify . SWITCH QDSnd SPCompleted $ connectionStats conn'
_ -> internalErr "ICQSndSecure: no switching queue found"
_ -> internalErr "ICQSndSecure: queue address not found in connection"
ICQDelete rId -> do
withServer $ \srv -> tryWithLock "ICQDelete" . withDuplexConn $ \(DuplexConnection cData rqs sqs) -> do
case removeQ (srv, rId) rqs of
Expand Down Expand Up @@ -1392,6 +1411,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
AM_QCONT_ -> notifyDel msgId err
AM_QADD_ -> qError msgId "QADD: AUTH"
AM_QKEY_ -> qError msgId "QKEY: AUTH"
AM_QSEC_ -> qError msgId "QKEY: AUTH"
AM_QUSE_ -> qError msgId "QUSE: AUTH"
AM_QTEST_ -> qError msgId "QTEST: AUTH"
AM_EREADY_ -> notifyDel msgId err
Expand Down Expand Up @@ -1445,8 +1465,13 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
AM_QKEY_ -> do
SomeConn _ conn <- withStore c (`getConn` connId)
notify . SWITCH QDSnd SPConfirmed $ connectionStats conn
AM_QSEC_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QSEC_" $ completeConnSwitch "QSEC" SSSendingQSEC
AM_QUSE_ -> pure ()
AM_QTEST_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QTEST_" $ do
AM_QTEST_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QTEST_" $ completeConnSwitch "QTEST" SSSendingQTEST
AM_EREADY_ -> pure ()
delMsgKeep (msgType == AM_A_MSG_) msgId
where
completeConnSwitch msgTag expectedStatus = do
withStore' c $ \db -> setSndQueueStatus db sq Active
SomeConn _ conn <- withStore c (`getConn` connId)
case conn of
Expand All @@ -1458,9 +1483,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
Just SndQueue {dbReplaceQueueId = Just replacedId, primary} ->
-- second part of this condition is a sanity check because dbReplaceQueueId cannot point to the same queue, see switchConnection'
case removeQP (\sq' -> dbQId sq' == replacedId && not (sameQueue addr sq')) sqs of
Nothing -> internalErr msgId "sent QTEST: queue not found in connection"
Nothing -> internalErr msgId $ "sent " <> msgTag <> ": queue not found in connection"
Just (sq', sq'' : sqs') -> do
checkSQSwchStatus sq' SSSendingQTEST
checkSQSwchStatus sq' expectedStatus
-- remove the delivery from the map to stop the thread when the delivery loop is complete
atomically $ TM.delete (qAddress sq') $ smpDeliveryWorkers c
withStore' c $ \db -> do
Expand All @@ -1470,12 +1495,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
let sqs'' = sq'' :| sqs'
conn' = DuplexConnection cData' rqs sqs''
notify . SWITCH QDSnd SPCompleted $ connectionStats conn'
_ -> internalErr msgId "sent QTEST: there is only one queue in connection"
_ -> internalErr msgId "sent QTEST: queue not in connection or not replacing another queue"
_ -> internalErr msgId "QTEST sent not in duplex connection"
AM_EREADY_ -> pure ()
delMsgKeep (msgType == AM_A_MSG_) msgId
where
_ -> internalErr msgId $ "sent " <> msgTag <> ": there is only one queue in connection"
_ -> internalErr msgId $ "sent " <> msgTag <> ": queue not in connection or not replacing another queue"
_ -> internalErr msgId $ msgTag <> " sent not in duplex connection"
setStatus status = do
withStore' c $ \db -> do
setSndQueueStatus db sq status
Expand Down Expand Up @@ -2249,8 +2271,9 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts)
(DuplexConnection _ rqs _, Just replacedId) -> do
when primary . withStore' c $ \db -> setRcvQueuePrimary db connId rq
case find ((replacedId ==) . dbQId) rqs of
Just rq'@RcvQueue {server, rcvId} -> do
checkRQSwchStatus rq' RSSendingQUSE
Just rq'@RcvQueue {server, rcvId, rcvSwchStatus} -> do
unless (rcvSwchStatus == Just RSSendingQUSE || rcvSwchStatus == Just RSSendingQADD) $
switchStatusError rq RSSendingQUSE rcvSwchStatus
void $ withStore' c $ \db -> setRcvSwitchStatus db rq' $ Just RSReceivedMessage
enqueueCommand c "" connId (Just server) $ AInternalCommand $ ICQDelete rcvId
_ -> notify . ERR . AGENT $ A_QUEUE "replaced RcvQueue not found in connection"
Expand All @@ -2271,6 +2294,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts)
A_QCONT addr -> qDuplexAckDel conn'' "QCONT" $ continueSending srvMsgId addr
QADD qs -> qDuplexAckDel conn'' "QADD" $ qAddMsg srvMsgId qs
QKEY qs -> qDuplexAckDel conn'' "QKEY" $ qKeyMsg srvMsgId qs
QSEC qs -> qDuplexAckDel conn'' "QSEC" $ qSecMsg srvMsgId qs
QUSE qs -> qDuplexAckDel conn'' "QUSE" $ qUseMsg srvMsgId qs
-- no action needed for QTEST
-- any message in the new queue will mark it active and trigger deletion of the old queue
Expand Down Expand Up @@ -2543,14 +2567,20 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts)
let (delSqs, keepSqs) = L.partition ((Just dbQueueId ==) . dbReplaceQId) sqs
case L.nonEmpty keepSqs of
Just sqs' -> do
(sq_@SndQueue {sndPublicKey}, dhPublicKey) <- lift $ newSndQueue userId connId qInfo
(sq_@SndQueue {sndId, sndPublicKey, sndSecure = sndSecure'}, dhPublicKey) <- lift $ newSndQueue userId connId qInfo
sq2 <- withStore c $ \db -> do
liftIO $ mapM_ (deleteConnSndQueue db connId) delSqs
addConnSndQueue db connId (sq_ :: NewSndQueue) {primary = True, dbReplaceQueueId = Just dbQueueId}
logServer "<--" c srv rId $ "MSG <QADD>:" <> logSecret srvMsgId <> " " <> logSecret (senderId queueAddress)
let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}}
void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', sndPublicKey)]
sq1 <- withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSendingQKEY
sq1 <-
if sndSecure'
then do
enqueueCommand c "" connId (Just $ qServer sq2) $ AInternalCommand $ ICQSndSecure sndId
withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSecuringQueue
else do
let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}}
void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', sndPublicKey)]
withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSendingQKEY
let sqs'' = updatedQs sq1 sqs' <> [sq2]
conn' = DuplexConnection cData' rqs sqs''
notify . SWITCH QDSnd SPStarted $ connectionStats conn'
Expand Down Expand Up @@ -2578,6 +2608,24 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts)
where
SMPQueueInfo cVer' SMPQueueAddress {smpServer, senderId, dhPublicKey} = qInfo

qSecMsg :: SMP.MsgId -> NonEmpty SMPQueueInfo -> Connection 'CDuplex -> AM ()
qSecMsg srvMsgId (qInfo :| _) conn'@(DuplexConnection cData' rqs _) = do
when (ratchetSyncSendProhibited cData') $ throwE $ AGENT (A_QUEUE "ratchet is not synchronized")
clientVRange <- asks $ smpClientVRange . config
unless (qInfo `isCompatible` clientVRange) . throwE $ AGENT A_VERSION
case findRQ (smpServer, senderId) rqs of
Just rq'@RcvQueue {e2ePrivKey = dhPrivKey, smpClientVersion = cVer, status = status'}
| status' == New || status' == Confirmed -> do
checkRQSwchStatus rq RSSendingQADD
logServer "<--" c srv rId $ "MSG <QSEC>:" <> logSecret srvMsgId <> " " <> logSecret senderId
let dhSecret = C.dh' dhPublicKey dhPrivKey
withStore' c $ \db -> setRcvQueueConfirmedE2E db rq' dhSecret $ min cVer cVer'
notify . SWITCH QDRcv SPCompleted $ connectionStats conn'
| otherwise -> qError "QSEC: queue already secured"
_ -> qError "QSEC: queue address not found in connection"
where
SMPQueueInfo cVer' SMPQueueAddress {smpServer, senderId, dhPublicKey} = qInfo

-- processed by queue sender
-- mark queue as Secured and to start sending messages to it
qUseMsg :: SMP.MsgId -> NonEmpty ((SMPServer, SMP.SenderId), Bool) -> Connection 'CDuplex -> AM ()
Expand Down
19 changes: 19 additions & 0 deletions src/Simplex/Messaging/Agent/Protocol.hs
Original file line number Diff line number Diff line change
Expand Up @@ -526,16 +526,22 @@ instance FromJSON RcvSwitchStatus where
data SndSwitchStatus
= SSSendingQKEY
| SSSendingQTEST
| SSSecuringQueue
| SSSendingQSEC
deriving (Eq, Show)

instance StrEncoding SndSwitchStatus where
strEncode = \case
SSSendingQKEY -> "sending_qkey"
SSSendingQTEST -> "sending_qtest"
SSSecuringQueue -> "securing_queue"
SSSendingQSEC -> "sending_qsec"
strP =
A.takeTill (== ' ') >>= \case
"sending_qkey" -> pure SSSendingQKEY
"sending_qtest" -> pure SSSendingQTEST
"securing_queue" -> pure SSSecuringQueue
"sending_qsec" -> pure SSSendingQSEC
_ -> fail "bad SndSwitchStatus"

instance ToField SndSwitchStatus where toField = toField . decodeLatin1 . strEncode
Expand Down Expand Up @@ -795,6 +801,7 @@ data AgentMessageType
| AM_QCONT_
| AM_QADD_
| AM_QKEY_
| AM_QSEC_
| AM_QUSE_
| AM_QTEST_
| AM_EREADY_
Expand All @@ -811,6 +818,7 @@ instance Encoding AgentMessageType where
AM_QCONT_ -> "QC"
AM_QADD_ -> "QA"
AM_QKEY_ -> "QK"
AM_QSEC_ -> "QS"
AM_QUSE_ -> "QU"
AM_QTEST_ -> "QT"
AM_EREADY_ -> "E"
Expand All @@ -827,6 +835,7 @@ instance Encoding AgentMessageType where
'C' -> pure AM_QCONT_
'A' -> pure AM_QADD_
'K' -> pure AM_QKEY_
'S' -> pure AM_QSEC_
'U' -> pure AM_QUSE_
'T' -> pure AM_QTEST_
_ -> fail "bad AgentMessageType"
Expand All @@ -849,6 +858,7 @@ agentMessageType = \case
A_QCONT _ -> AM_QCONT_
QADD _ -> AM_QADD_
QKEY _ -> AM_QKEY_
QSEC _ -> AM_QSEC_
QUSE _ -> AM_QUSE_
QTEST _ -> AM_QTEST_
EREADY _ -> AM_EREADY_
Expand All @@ -873,6 +883,7 @@ data AMsgType
| A_QCONT_
| QADD_
| QKEY_
| QSEC_
| QUSE_
| QTEST_
| EREADY_
Expand All @@ -886,6 +897,7 @@ instance Encoding AMsgType where
A_QCONT_ -> "QC"
QADD_ -> "QA"
QKEY_ -> "QK"
QSEC_ -> "QS"
QUSE_ -> "QU"
QTEST_ -> "QT"
EREADY_ -> "E"
Expand All @@ -899,6 +911,7 @@ instance Encoding AMsgType where
'C' -> pure A_QCONT_
'A' -> pure QADD_
'K' -> pure QKEY_
'S' -> pure QSEC_
'U' -> pure QUSE_
'T' -> pure QTEST_
_ -> fail "bad AMsgType"
Expand All @@ -921,6 +934,10 @@ data AMessage
QADD (NonEmpty (SMPQueueUri, Maybe SndQAddr))
| -- key to secure the added queues and agree e2e encryption key (sent by sender)
QKEY (NonEmpty (SMPQueueInfo, SndPublicAuthKey))
| -- sent by the sender who secured the queue with SKEY (SMP protocol v9).
-- This message is needed to agree shared secret - it completes switching.
-- This message requires a new envelope that is sent together with public DH key.
QSEC (NonEmpty SMPQueueInfo)
| -- inform that the queues are ready to use (sent by recipient)
QUSE (NonEmpty (SndQAddr, Bool))
| -- sent by the sender to test new queues and to complete switching
Expand Down Expand Up @@ -977,6 +994,7 @@ instance Encoding AMessage where
A_QCONT addr -> smpEncode (A_QCONT_, addr)
QADD qs -> smpEncode (QADD_, qs)
QKEY qs -> smpEncode (QKEY_, qs)
QSEC qs -> smpEncode (QSEC_, qs)
QUSE qs -> smpEncode (QUSE_, qs)
QTEST qs -> smpEncode (QTEST_, qs)
EREADY lastDecryptedMsgId -> smpEncode (EREADY_, lastDecryptedMsgId)
Expand All @@ -989,6 +1007,7 @@ instance Encoding AMessage where
A_QCONT_ -> A_QCONT <$> smpP
QADD_ -> QADD <$> smpP
QKEY_ -> QKEY <$> smpP
QSEC_ -> QSEC <$> smpP
QUSE_ -> QUSE <$> smpP
QTEST_ -> QTEST <$> smpP
EREADY_ -> EREADY <$> smpP
Expand Down
Loading

0 comments on commit b48b86f

Please sign in to comment.