Skip to content

Commit e71cb4c

Browse files
committed
agent: attempt at faster queue rotation (does not work)
1 parent ae4325d commit e71cb4c

File tree

5 files changed

+98
-18
lines changed

5 files changed

+98
-18
lines changed

protocol/diagrams/duplex-messaging/queue-rotation-fast.mmd

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ sequenceDiagram
99
A ->> S: SEND: QADD (R'): send address<br>of the new queue(s)
1010
S ->> B: MSG: QADD (R')
1111
B ->> R': SKEY: secure new queue
12-
B ->> R': SEND: QTEST
13-
R' ->> A: MSG: QTEST
12+
B ->> R': SEND: QSEC: to agree shared secret
13+
R' ->> A: MSG: QSEC
1414
A ->> R: DEL: delete the old queue
1515
B ->> R': SEND: send messages to the new queue
1616
R' ->> A: MSG: receive messages from the new queue

rfcs/2024-06-14-fast-connection.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,18 @@ These are the proposed changes:
3131
5. Accepting client will secure the messaging queue before sending the confirmation to it.
3232
6. Initiating client will secure the messaging queue before sending the confirmation.
3333

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

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

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

40+
## Queue rotation
41+
42+
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.
43+
44+
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.
45+
4046
## Implementation questions
4147

4248
Currently we store received confirmations in the database, so that the client can confirm them. This becomes unnecessary.

src/Simplex/Messaging/Agent.hs

Lines changed: 63 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1195,6 +1195,25 @@ runCommandProcessing c@AgentClient {subQ} server_ Worker {doWork} = do
11951195
notify . SWITCH QDRcv SPSecured $ connectionStats conn'
11961196
_ -> internalErr "ICQSecure: no switching queue found"
11971197
_ -> internalErr "ICQSecure: queue address not found in connection"
1198+
ICQSndSecure sId ->
1199+
withServer $ \srv -> tryWithLock "ICQSndSecure" . withDuplexConn $ \(DuplexConnection cData rqs sqs) ->
1200+
case find (sameQueue (srv, sId)) sqs of
1201+
Just sq'@SndQueue {server, sndId, sndSecure, status, smpClientVersion, e2ePubKey = Just dhPublicKey, dbReplaceQueueId = Just replaceQId} ->
1202+
case find ((replaceQId ==) . dbQId) sqs of
1203+
Just sq1 -> when (status == New) $ do
1204+
secureSndQueue c sq'
1205+
withStore' c $ \db -> setSndQueueStatus db sq' Secured
1206+
let sq'' = (sq' :: SndQueue) {status = Secured}
1207+
queueAddress = SMPQueueAddress {smpServer = server, senderId = sndId, dhPublicKey, sndSecure}
1208+
qInfo = SMPQueueInfo {clientVersion = smpClientVersion, queueAddress}
1209+
-- sending QSEC to the new queue only, the old one will be removed if sent successfully
1210+
void . enqueueMessages c cData [sq''] SMP.noMsgFlags $ QSEC [qInfo]
1211+
sq1' <- withStore' c $ \db -> setSndSwitchStatus db sq1 $ Just SSSendingQSEC
1212+
let sqs' = updatedQs sq1' sqs
1213+
conn' = DuplexConnection cData rqs sqs'
1214+
notify . SWITCH QDSnd SPCompleted $ connectionStats conn'
1215+
_ -> internalErr "ICQSndSecure: no switching queue found"
1216+
_ -> internalErr "ICQSndSecure: queue address not found in connection"
11981217
ICQDelete rId -> do
11991218
withServer $ \srv -> tryWithLock "ICQDelete" . withDuplexConn $ \(DuplexConnection cData rqs sqs) -> do
12001219
case removeQ (srv, rId) rqs of
@@ -1393,6 +1412,7 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
13931412
AM_QCONT_ -> notifyDel msgId err
13941413
AM_QADD_ -> qError msgId "QADD: AUTH"
13951414
AM_QKEY_ -> qError msgId "QKEY: AUTH"
1415+
AM_QSEC_ -> qError msgId "QKEY: AUTH"
13961416
AM_QUSE_ -> qError msgId "QUSE: AUTH"
13971417
AM_QTEST_ -> qError msgId "QTEST: AUTH"
13981418
AM_EREADY_ -> notifyDel msgId err
@@ -1446,8 +1466,13 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
14461466
AM_QKEY_ -> do
14471467
SomeConn _ conn <- withStore c (`getConn` connId)
14481468
notify . SWITCH QDSnd SPConfirmed $ connectionStats conn
1469+
AM_QSEC_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QSEC_" $ completeConnSwitch "QSEC" SSSendingQSEC
14491470
AM_QUSE_ -> pure ()
1450-
AM_QTEST_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QTEST_" $ do
1471+
AM_QTEST_ -> withConnLock c connId "runSmpQueueMsgDelivery AM_QTEST_" $ completeConnSwitch "QTEST" SSSendingQTEST
1472+
AM_EREADY_ -> pure ()
1473+
delMsgKeep (msgType == AM_A_MSG_) msgId
1474+
where
1475+
completeConnSwitch msgTag expectedStatus = do
14511476
withStore' c $ \db -> setSndQueueStatus db sq Active
14521477
SomeConn _ conn <- withStore c (`getConn` connId)
14531478
case conn of
@@ -1459,9 +1484,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
14591484
Just SndQueue {dbReplaceQueueId = Just replacedId, primary} ->
14601485
-- second part of this condition is a sanity check because dbReplaceQueueId cannot point to the same queue, see switchConnection'
14611486
case removeQP (\sq' -> dbQId sq' == replacedId && not (sameQueue addr sq')) sqs of
1462-
Nothing -> internalErr msgId "sent QTEST: queue not found in connection"
1487+
Nothing -> internalErr msgId $ "sent " <> msgTag <> ": queue not found in connection"
14631488
Just (sq', sq'' : sqs') -> do
1464-
checkSQSwchStatus sq' SSSendingQTEST
1489+
checkSQSwchStatus sq' expectedStatus
14651490
-- remove the delivery from the map to stop the thread when the delivery loop is complete
14661491
atomically $ TM.delete (qAddress sq') $ smpDeliveryWorkers c
14671492
withStore' c $ \db -> do
@@ -1471,12 +1496,9 @@ runSmpQueueMsgDelivery c@AgentClient {subQ} ConnData {connId} sq@SndQueue {userI
14711496
let sqs'' = sq'' :| sqs'
14721497
conn' = DuplexConnection cData' rqs sqs''
14731498
notify . SWITCH QDSnd SPCompleted $ connectionStats conn'
1474-
_ -> internalErr msgId "sent QTEST: there is only one queue in connection"
1475-
_ -> internalErr msgId "sent QTEST: queue not in connection or not replacing another queue"
1476-
_ -> internalErr msgId "QTEST sent not in duplex connection"
1477-
AM_EREADY_ -> pure ()
1478-
delMsgKeep (msgType == AM_A_MSG_) msgId
1479-
where
1499+
_ -> internalErr msgId $ "sent " <> msgTag <> ": there is only one queue in connection"
1500+
_ -> internalErr msgId $ "sent " <> msgTag <> ": queue not in connection or not replacing another queue"
1501+
_ -> internalErr msgId $ msgTag <> " sent not in duplex connection"
14801502
setStatus status = do
14811503
withStore' c $ \db -> do
14821504
setSndQueueStatus db sq status
@@ -2249,8 +2271,9 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts)
22492271
(DuplexConnection _ rqs _, Just replacedId) -> do
22502272
when primary . withStore' c $ \db -> setRcvQueuePrimary db connId rq
22512273
case find ((replacedId ==) . dbQId) rqs of
2252-
Just rq'@RcvQueue {server, rcvId} -> do
2253-
checkRQSwchStatus rq' RSSendingQUSE
2274+
Just rq'@RcvQueue {server, rcvId, rcvSwchStatus} -> do
2275+
unless (rcvSwchStatus == Just RSSendingQUSE || rcvSwchStatus == Just RSSendingQADD) $
2276+
switchStatusError rq RSSendingQUSE rcvSwchStatus
22542277
void $ withStore' c $ \db -> setRcvSwitchStatus db rq' $ Just RSReceivedMessage
22552278
enqueueCommand c "" connId (Just server) $ AInternalCommand $ ICQDelete rcvId
22562279
_ -> notify . ERR . AGENT $ A_QUEUE "replaced RcvQueue not found in connection"
@@ -2271,6 +2294,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts)
22712294
A_QCONT addr -> qDuplexAckDel conn'' "QCONT" $ continueSending srvMsgId addr
22722295
QADD qs -> qDuplexAckDel conn'' "QADD" $ qAddMsg srvMsgId qs
22732296
QKEY qs -> qDuplexAckDel conn'' "QKEY" $ qKeyMsg srvMsgId qs
2297+
QSEC qs -> qDuplexAckDel conn'' "QSEC" $ qSecMsg srvMsgId qs
22742298
QUSE qs -> qDuplexAckDel conn'' "QUSE" $ qUseMsg srvMsgId qs
22752299
-- no action needed for QTEST
22762300
-- any message in the new queue will mark it active and trigger deletion of the old queue
@@ -2543,14 +2567,20 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts)
25432567
let (delSqs, keepSqs) = L.partition ((Just dbQueueId ==) . dbReplaceQId) sqs
25442568
case L.nonEmpty keepSqs of
25452569
Just sqs' -> do
2546-
(sq_@SndQueue {sndPublicKey}, dhPublicKey) <- lift $ newSndQueue userId connId qInfo
2570+
(sq_@SndQueue {sndId, sndPublicKey, sndSecure = sndSecure'}, dhPublicKey) <- lift $ newSndQueue userId connId qInfo
25472571
sq2 <- withStore c $ \db -> do
25482572
liftIO $ mapM_ (deleteConnSndQueue db connId) delSqs
25492573
addConnSndQueue db connId (sq_ :: NewSndQueue) {primary = True, dbReplaceQueueId = Just dbQueueId}
25502574
logServer "<--" c srv rId $ "MSG <QADD>:" <> logSecret srvMsgId <> " " <> logSecret (senderId queueAddress)
2551-
let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}}
2552-
void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', sndPublicKey)]
2553-
sq1 <- withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSendingQKEY
2575+
sq1 <-
2576+
if sndSecure'
2577+
then do
2578+
enqueueCommand c "" connId (Just $ qServer sq2) $ AInternalCommand $ ICQSndSecure sndId
2579+
withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSecuringQueue
2580+
else do
2581+
let sqInfo' = (sqInfo :: SMPQueueInfo) {queueAddress = queueAddress {dhPublicKey}}
2582+
void . enqueueMessages c cData' sqs SMP.noMsgFlags $ QKEY [(sqInfo', sndPublicKey)]
2583+
withStore' c $ \db -> setSndSwitchStatus db sq $ Just SSSendingQKEY
25542584
let sqs'' = updatedQs sq1 sqs' <> [sq2]
25552585
conn' = DuplexConnection cData' rqs sqs''
25562586
notify . SWITCH QDSnd SPStarted $ connectionStats conn'
@@ -2578,6 +2608,24 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(_, srv, _), _v, sessId, ts)
25782608
where
25792609
SMPQueueInfo cVer' SMPQueueAddress {smpServer, senderId, dhPublicKey} = qInfo
25802610

2611+
qSecMsg :: SMP.MsgId -> NonEmpty SMPQueueInfo -> Connection 'CDuplex -> AM ()
2612+
qSecMsg srvMsgId (qInfo :| _) conn'@(DuplexConnection cData' rqs _) = do
2613+
when (ratchetSyncSendProhibited cData') $ throwE $ AGENT (A_QUEUE "ratchet is not synchronized")
2614+
clientVRange <- asks $ smpClientVRange . config
2615+
unless (qInfo `isCompatible` clientVRange) . throwE $ AGENT A_VERSION
2616+
case findRQ (smpServer, senderId) rqs of
2617+
Just rq'@RcvQueue {e2ePrivKey = dhPrivKey, smpClientVersion = cVer, status = status'}
2618+
| status' == New || status' == Confirmed -> do
2619+
checkRQSwchStatus rq RSSendingQADD
2620+
logServer "<--" c srv rId $ "MSG <QSEC>:" <> logSecret srvMsgId <> " " <> logSecret senderId
2621+
let dhSecret = C.dh' dhPublicKey dhPrivKey
2622+
withStore' c $ \db -> setRcvQueueConfirmedE2E db rq' dhSecret $ min cVer cVer'
2623+
notify . SWITCH QDRcv SPCompleted $ connectionStats conn'
2624+
| otherwise -> qError "QSEC: queue already secured"
2625+
_ -> qError "QSEC: queue address not found in connection"
2626+
where
2627+
SMPQueueInfo cVer' SMPQueueAddress {smpServer, senderId, dhPublicKey} = qInfo
2628+
25812629
-- processed by queue sender
25822630
-- mark queue as Secured and to start sending messages to it
25832631
qUseMsg :: SMP.MsgId -> NonEmpty ((SMPServer, SMP.SenderId), Bool) -> Connection 'CDuplex -> AM ()

src/Simplex/Messaging/Agent/Protocol.hs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,16 +526,22 @@ instance FromJSON RcvSwitchStatus where
526526
data SndSwitchStatus
527527
= SSSendingQKEY
528528
| SSSendingQTEST
529+
| SSSecuringQueue
530+
| SSSendingQSEC
529531
deriving (Eq, Show)
530532

531533
instance StrEncoding SndSwitchStatus where
532534
strEncode = \case
533535
SSSendingQKEY -> "sending_qkey"
534536
SSSendingQTEST -> "sending_qtest"
537+
SSSecuringQueue -> "securing_queue"
538+
SSSendingQSEC -> "sending_qsec"
535539
strP =
536540
A.takeTill (== ' ') >>= \case
537541
"sending_qkey" -> pure SSSendingQKEY
538542
"sending_qtest" -> pure SSSendingQTEST
543+
"securing_queue" -> pure SSSecuringQueue
544+
"sending_qsec" -> pure SSSendingQSEC
539545
_ -> fail "bad SndSwitchStatus"
540546

541547
instance ToField SndSwitchStatus where toField = toField . decodeLatin1 . strEncode
@@ -795,6 +801,7 @@ data AgentMessageType
795801
| AM_QCONT_
796802
| AM_QADD_
797803
| AM_QKEY_
804+
| AM_QSEC_
798805
| AM_QUSE_
799806
| AM_QTEST_
800807
| AM_EREADY_
@@ -811,6 +818,7 @@ instance Encoding AgentMessageType where
811818
AM_QCONT_ -> "QC"
812819
AM_QADD_ -> "QA"
813820
AM_QKEY_ -> "QK"
821+
AM_QSEC_ -> "QS"
814822
AM_QUSE_ -> "QU"
815823
AM_QTEST_ -> "QT"
816824
AM_EREADY_ -> "E"
@@ -827,6 +835,7 @@ instance Encoding AgentMessageType where
827835
'C' -> pure AM_QCONT_
828836
'A' -> pure AM_QADD_
829837
'K' -> pure AM_QKEY_
838+
'S' -> pure AM_QSEC_
830839
'U' -> pure AM_QUSE_
831840
'T' -> pure AM_QTEST_
832841
_ -> fail "bad AgentMessageType"
@@ -849,6 +858,7 @@ agentMessageType = \case
849858
A_QCONT _ -> AM_QCONT_
850859
QADD _ -> AM_QADD_
851860
QKEY _ -> AM_QKEY_
861+
QSEC _ -> AM_QSEC_
852862
QUSE _ -> AM_QUSE_
853863
QTEST _ -> AM_QTEST_
854864
EREADY _ -> AM_EREADY_
@@ -873,6 +883,7 @@ data AMsgType
873883
| A_QCONT_
874884
| QADD_
875885
| QKEY_
886+
| QSEC_
876887
| QUSE_
877888
| QTEST_
878889
| EREADY_
@@ -886,6 +897,7 @@ instance Encoding AMsgType where
886897
A_QCONT_ -> "QC"
887898
QADD_ -> "QA"
888899
QKEY_ -> "QK"
900+
QSEC_ -> "QS"
889901
QUSE_ -> "QU"
890902
QTEST_ -> "QT"
891903
EREADY_ -> "E"
@@ -899,6 +911,7 @@ instance Encoding AMsgType where
899911
'C' -> pure A_QCONT_
900912
'A' -> pure QADD_
901913
'K' -> pure QKEY_
914+
'S' -> pure QSEC_
902915
'U' -> pure QUSE_
903916
'T' -> pure QTEST_
904917
_ -> fail "bad AMsgType"
@@ -921,6 +934,10 @@ data AMessage
921934
QADD (NonEmpty (SMPQueueUri, Maybe SndQAddr))
922935
| -- key to secure the added queues and agree e2e encryption key (sent by sender)
923936
QKEY (NonEmpty (SMPQueueInfo, SndPublicAuthKey))
937+
| -- sent by the sender who secured the queue with SKEY (SMP protocol v9).
938+
-- This message is needed to agree shared secret - it completes switching.
939+
-- This message requires a new envelope that is sent together with public DH key.
940+
QSEC (NonEmpty SMPQueueInfo)
924941
| -- inform that the queues are ready to use (sent by recipient)
925942
QUSE (NonEmpty (SndQAddr, Bool))
926943
| -- sent by the sender to test new queues and to complete switching
@@ -977,6 +994,7 @@ instance Encoding AMessage where
977994
A_QCONT addr -> smpEncode (A_QCONT_, addr)
978995
QADD qs -> smpEncode (QADD_, qs)
979996
QKEY qs -> smpEncode (QKEY_, qs)
997+
QSEC qs -> smpEncode (QSEC_, qs)
980998
QUSE qs -> smpEncode (QUSE_, qs)
981999
QTEST qs -> smpEncode (QTEST_, qs)
9821000
EREADY lastDecryptedMsgId -> smpEncode (EREADY_, lastDecryptedMsgId)
@@ -989,6 +1007,7 @@ instance Encoding AMessage where
9891007
A_QCONT_ -> A_QCONT <$> smpP
9901008
QADD_ -> QADD <$> smpP
9911009
QKEY_ -> QKEY <$> smpP
1010+
QSEC_ -> QSEC <$> smpP
9921011
QUSE_ -> QUSE <$> smpP
9931012
QTEST_ -> QTEST <$> smpP
9941013
EREADY_ -> EREADY <$> smpP

src/Simplex/Messaging/Agent/Store.hs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,7 @@ data InternalCommand
382382
| ICDeleteConn
383383
| ICDeleteRcvQueue SMP.RecipientId
384384
| ICQSecure SMP.RecipientId SMP.SndPublicAuthKey
385+
| ICQSndSecure SMP.SenderId
385386
| ICQDelete SMP.RecipientId
386387

387388
data InternalCommandTag
@@ -392,6 +393,7 @@ data InternalCommandTag
392393
| ICDeleteConn_
393394
| ICDeleteRcvQueue_
394395
| ICQSecure_
396+
| ICQSndSecure_
395397
| ICQDelete_
396398
deriving (Show)
397399

@@ -404,6 +406,7 @@ instance StrEncoding InternalCommand where
404406
ICDeleteConn -> strEncode ICDeleteConn_
405407
ICDeleteRcvQueue rId -> strEncode (ICDeleteRcvQueue_, rId)
406408
ICQSecure rId senderKey -> strEncode (ICQSecure_, rId, senderKey)
409+
ICQSndSecure sId -> strEncode (ICQSndSecure_, sId)
407410
ICQDelete rId -> strEncode (ICQDelete_, rId)
408411
strP =
409412
strP >>= \case
@@ -414,6 +417,7 @@ instance StrEncoding InternalCommand where
414417
ICDeleteConn_ -> pure ICDeleteConn
415418
ICDeleteRcvQueue_ -> ICDeleteRcvQueue <$> _strP
416419
ICQSecure_ -> ICQSecure <$> _strP <*> _strP
420+
ICQSndSecure_ -> ICQSndSecure <$> _strP
417421
ICQDelete_ -> ICQDelete <$> _strP
418422

419423
instance StrEncoding InternalCommandTag where
@@ -425,6 +429,7 @@ instance StrEncoding InternalCommandTag where
425429
ICDeleteConn_ -> "DELETE_CONN"
426430
ICDeleteRcvQueue_ -> "DELETE_RCV_QUEUE"
427431
ICQSecure_ -> "QSECURE"
432+
ICQSndSecure_ -> "QSND_SECURE"
428433
ICQDelete_ -> "QDELETE"
429434
strP =
430435
A.takeTill (== ' ') >>= \case
@@ -435,6 +440,7 @@ instance StrEncoding InternalCommandTag where
435440
"DELETE_CONN" -> pure ICDeleteConn_
436441
"DELETE_RCV_QUEUE" -> pure ICDeleteRcvQueue_
437442
"QSECURE" -> pure ICQSecure_
443+
"QSND_SECURE" -> pure ICQSndSecure_
438444
"QDELETE" -> pure ICQDelete_
439445
_ -> fail "bad InternalCommandTag"
440446

@@ -452,6 +458,7 @@ internalCmdTag = \case
452458
ICDeleteConn -> ICDeleteConn_
453459
ICDeleteRcvQueue {} -> ICDeleteRcvQueue_
454460
ICQSecure {} -> ICQSecure_
461+
ICQSndSecure {} -> ICQSndSecure_
455462
ICQDelete _ -> ICQDelete_
456463

457464
-- * Confirmation types

0 commit comments

Comments
 (0)