From ead8f10f26503a43fadcb98fc31f0e906914364c Mon Sep 17 00:00:00 2001 From: Evgeny Date: Sat, 3 Oct 2026 08:56:19 +0100 Subject: [PATCH] smp protocol: bind agreed version of forwarded command (#1915) * smp protocol: bind agreed version of forwarded command * refactor * pass proxied relay version range via config * simplify * refactor * packZipWith --------- Co-authored-by: Evgeny @ SimpleX Chat <259188159+evgeny-simplex@users.noreply.github.com> --- protocol/simplex-messaging.md | 12 ++++++---- src/Simplex/Messaging/Client.hs | 28 +++++++++++++--------- src/Simplex/Messaging/Protocol.hs | 28 +++++++++++++++------- src/Simplex/Messaging/Server.hs | 12 ++++++---- src/Simplex/Messaging/Transport.hs | 19 +++++++++------ tests/AgentTests/FunctionalAPITests.hs | 6 ++++- tests/SMPClient.hs | 10 ++++++++ tests/SMPProxyTests.hs | 32 ++++++++++++++++++++++++-- 8 files changed, 109 insertions(+), 38 deletions(-) diff --git a/protocol/simplex-messaging.md b/protocol/simplex-messaging.md index c5e5d8489..684874ee5 100644 --- a/protocol/simplex-messaging.md +++ b/protocol/simplex-messaging.md @@ -86,7 +86,7 @@ It's designed with the focus on communication security and integrity, under the It is designed as a low level protocol for other application protocols to solve the problem of secure and private message transmission, making [MITM attack][1] very difficult at any part of the message transmission system. -This document describes SMP protocol version 22. Versions 1-5 are discontinued. The version history: +This document describes SMP protocol version 23. Versions 1-5 are discontinued. The version history: - v1: binary protocol encoding - v2: message flags (used to control notifications) @@ -109,6 +109,7 @@ This document describes SMP protocol version 22. Versions 1-5 are discontinued. - v20: public namespaces resolver (RSLV command, RNAME response) — direct or forwarded via PFWD - v21: server public information in handshake - v22: `RNAME` says whether a name can be registered, not only what it resolves to +- v23: version in nonces of forwarded commands, random nonces for forwarded responses ## Introduction @@ -1128,7 +1129,7 @@ The proxy router may respond with error response in case the destination router Sender can send `SKEY` and `SEND` commands via proxy after obtaining the session ID with `PRXY` command (see [Request proxied session](#request-proxied-session)). -Transmission sent to proxy router should use session ID as entity ID and use a random correlation ID of 24 bytes as a nonce for crypto_box encryption of transmission to the destination router. The random ephemeral X25519 key to encrypt transmission should be unique per command, and it should be combined with the key sent by the router in the handshake header to proxy and to the client in `PKEY` command. +Transmission sent to proxy router should use session ID as entity ID and use a random correlation ID of 24 bytes as a nonce for crypto_box encryption of transmission to the destination router. When `smpVersion` in `PFWD` is 23 or higher, the first 2 bytes of this nonce are XOR-ed with `smpVersion`. The random ephemeral X25519 key to encrypt transmission should be unique per command, and it should be combined with the key sent by the router in the handshake header to proxy and to the client in `PKEY` command. Encrypted transmission should use the received session ID from the connection between proxy router and destination router in the authorized body. @@ -1140,10 +1141,11 @@ commandKey = length x509encoded The proxy router will forward the encrypted transmission in `RFWD` command (see below). -Having received the `RRES` response from the destination router, proxy router will forward `PRES` response to the client. `PRES` response should use the same correlation ID as `PFWD` command. The destination router will use this correlation ID increased by 1 as a nonce for encryption of the response. +Having received the `RRES` response from the destination router, proxy router will forward `PRES` response to the client. `PRES` response should use the same correlation ID as `PFWD` command. The destination router will use this correlation ID increased by 1 as a nonce for encryption of the response. When `smpVersion` in `PFWD` is 23 or higher, the destination router uses a random nonce instead, and sends it before the encrypted response. ```abnf -proxyResponse = %s"PRES" SP +proxyResponse = %s"PRES" SP [responseNonce] +responseNonce = %s"0" / (%s"1" 24*24 OCTET) ; from v23 forwardedResponse = *OCTET ; client-encrypted SMP response, decrypted by client using per-command DH secret ``` @@ -1170,7 +1172,7 @@ The shared secret for encrypting transmission bodies between proxy router and de ```abnf -relayResponse = %s"RRES" SP +relayResponse = %s"RRES" SP [responseNonce] responseTransmission = fwdCorrId forwardedResponse ; fwdCorrId and forwardedResponse defined above in RFWD section ``` diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index f8f1a4cb9..7a2eedeb9 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -195,6 +195,7 @@ data PClient v err msg = PClient transportHost :: TransportHost, tcpConnectTimeout :: NetworkTimeout, tcpTimeout :: NetworkTimeout, + proxiedRelayVRange :: VersionRange v, sendPings :: TVar Bool, lastReceived :: TVar UTCTime, timeoutErrorCount :: TVar Int, @@ -240,6 +241,7 @@ smpClientStub g sessionId thVersion thAuth = do transportHost = "localhost", tcpConnectTimeout, tcpTimeout, + proxiedRelayVRange = supportedClientSMPRelayVRange, sendPings, lastReceived, timeoutErrorCount, @@ -477,6 +479,7 @@ data ProtocolClientConfig v = ProtocolClientConfig serviceCredentials :: Maybe ServiceCredentials, -- | client-server protocol version range serverVRange :: VersionRange v, + proxiedRelayVRange :: VersionRange v, -- | agree shared session secret (used in SMP proxy for additional encryption layer) agreeSecret :: Bool, -- | Whether connecting client is a proxy server. See comment in ClientHandshake @@ -495,6 +498,7 @@ defaultClientConfig clientALPN useSNI serverVRange = clientALPN, serviceCredentials = Nothing, serverVRange, + proxiedRelayVRange = serverVRange, agreeSecret = False, proxyServer = False, useSNI @@ -505,6 +509,7 @@ defaultSMPClientConfig :: ProtocolClientConfig SMPVersion defaultSMPClientConfig = (defaultClientConfig (Just alpnSupportedSMPHandshakes) False supportedClientSMPRelayVRange) { defaultTransport = (show defaultSMPPort, transport @TLS), + proxiedRelayVRange = supportedClientSMPRelayVRange, agreeSecret = True } {-# INLINE defaultSMPClientConfig #-} @@ -568,7 +573,7 @@ type SMPTransportSession = TransportSession BrokerMsg -- A single queue can be used for multiple 'SMPClient' instances, -- as 'SMPServerTransmission' includes server information. getProtocolClient :: forall v err msg. Protocol v err msg => TVar ChaChaDRG -> NetworkRequestMode -> TransportSession msg -> ProtocolClientConfig v -> [HostName] -> Maybe (TBQueue (ServerTransmissionBatch v err msg)) -> UTCTime -> (ProtocolClient v err msg -> IO ()) -> IO (Either (ProtocolClientError err) (ProtocolClient v err msg)) -getProtocolClient g nm transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serviceCredentials, serverVRange, agreeSecret, proxyServer, useSNI} presetDomains msgQ proxySessTs disconnected = do +getProtocolClient g nm transportSession@(_, srv, _) cfg@ProtocolClientConfig {qSize, networkConfig, clientALPN, serviceCredentials, serverVRange, proxiedRelayVRange, agreeSecret, proxyServer, useSNI} presetDomains msgQ proxySessTs disconnected = do case chooseTransportHost networkConfig (host srv) of Right useHost -> (getCurrentTime >>= mkProtocolClient useHost >>= runClient useTransport useHost) @@ -593,6 +598,7 @@ getProtocolClient g nm transportSession@(_, srv, _) cfg@ProtocolClientConfig {qS transportHost, tcpConnectTimeout, tcpTimeout, + proxiedRelayVRange, sendPings, lastReceived, timeoutErrorCount, @@ -1115,10 +1121,10 @@ deleteSMPQueues = okSMPCommands DEL -- send PRXY :: SMPServer -> Maybe BasicAuth -> Command Sender -- receives PKEY :: SessionId -> X.CertificateChain -> X.SignedExact X.PubKey -> BrokerMsg connectSMPProxiedRelay :: SMPClient -> NetworkRequestMode -> SMPServer -> Maybe BasicAuth -> ExceptT SMPClientError IO ProxiedRelay -connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, tcpTimeout}} nm relayServ@ProtocolServer {port = relayPort, keyHash = C.KeyHash kh} proxyAuth = +connectSMPProxiedRelay c@ProtocolClient {client_ = PClient {tcpConnectTimeout, tcpTimeout, proxiedRelayVRange}} nm relayServ@ProtocolServer {port = relayPort, keyHash = C.KeyHash kh} proxyAuth = sendProtocolCommand_ c nm Nothing tOut Nothing NoEntity (Cmd SProxiedClient (PRXY relayServ proxyAuth)) >>= \case PKEY sId vr (CertChainPubKey chain key) -> - case supportedClientSMPRelayVRange `compatibleVersion` vr of + case proxiedRelayVRange `compatibleVersion` vr of Nothing -> throwE $ transportErr TEVersion Just (Compatible v) -> do relayKey <- liftEitherWith (const $ transportErr $ TEHandshake IDENTITY) =<< liftIO (runExceptT $ validateRelay chain key) @@ -1169,7 +1175,7 @@ instance StrEncoding ProxyClientError where -- consider how to process slow responses - is it handled somehow locally or delegated to the caller -- this method is used in the client -- sends PFWD :: C.PublicKeyX25519 -> EncTransmission -> Command Sender --- receives PRES :: EncResponse -> BrokerMsg -- proxy to client +-- receives PRES :: Maybe C.CbNonce -> EncResponse -> BrokerMsg -- proxy to client -- When client sends message via proxy, there may be one successful scenario and 9 error scenarios -- as shown below (WTF stands for unexpected response, ??? for response that failed to parse). @@ -1228,14 +1234,14 @@ proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c TBError e _ : _ -> throwE $ PCETransportError e TBTransmission s _ : _ -> pure s TBTransmissions s _ _ : _ -> pure s - et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret nonce b paddedProxiedTLength + et <- liftEitherWith PCECryptoError $ EncTransmission <$> C.cbEncrypt cmdSecret (encTransmissionNonce v nonce) b paddedProxiedTLength -- proxy interaction errors are wrapped let tOut = Just $ 2 * netTimeoutInt tcpTimeout nm tryE (sendProtocolCommand_ c nm (Just nonce) tOut Nothing (EntityId sessionId) (Cmd SProxiedClient (PFWD v cmdPubKey et))) >>= \case Right r -> case r of - PRES (EncResponse er) -> do + PRES nonce_ (EncResponse er) -> do -- server interaction errors are thrown directly - t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (C.reverseNonce nonce) er + t' <- liftEitherWith PCECryptoError $ C.cbDecrypt cmdSecret (fromMaybe (C.reverseNonce nonce) nonce_) er case tParse serverThParams t' of t'' :| [] -> case tDecodeClient serverThParams t'' of (_, _, cmd) -> case cmd of @@ -1253,10 +1259,10 @@ proxySMPCommand c@ProtocolClient {thParams = proxyThParams, client_ = PClient {c -- this method is used in the proxy -- sends RFWD :: EncFwdTransmission -> Command Sender --- receives RRES :: EncFwdResponse -> BrokerMsg +-- receives RRES :: Maybe C.CbNonce -> EncFwdResponse -> BrokerMsg -- proxy should send PRES to the client with EncResponse -- Always uses background timeout mode -forwardSMPTransmission :: SMPClient -> CorrId -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO EncResponse +forwardSMPTransmission :: SMPClient -> CorrId -> VersionSMP -> C.PublicKeyX25519 -> EncTransmission -> ExceptT SMPClientError IO (Maybe C.CbNonce, EncResponse) forwardSMPTransmission c@ProtocolClient {thParams, client_ = PClient {clientCorrId = g}} fwdCorrId fwdVersion fwdKey fwdTransmission = do -- prepare params sessSecret <- case thAuth thParams of @@ -1268,11 +1274,11 @@ forwardSMPTransmission c@ProtocolClient {thParams, client_ = PClient {clientCorr eft = EncFwdTransmission $ C.cbEncryptNoPad sessSecret nonce (smpEncode fwdT) -- send sendProtocolCommand_ c NRMBackground (Just nonce) Nothing Nothing NoEntity (Cmd SProxyService (RFWD eft)) >>= \case - RRES (EncFwdResponse efr) -> do + RRES nonce_ (EncFwdResponse efr) -> do -- unwrap r' <- liftEitherWith PCECryptoError $ C.cbDecryptNoPad sessSecret (C.reverseNonce nonce) efr FwdResponse {fwdCorrId = _, fwdResponse} <- liftEitherWith (const $ PCEResponseError BLOCK) $ smpDecode r' - pure fwdResponse + pure (nonce_, fwdResponse) r -> throwE $ unexpectedResponse r -- get queue information - always sent interactively diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 14f2a967f..a62e84224 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -168,6 +168,7 @@ module Simplex.Messaging.Protocol EncFwdTransmission (..), EncResponse (..), EncTransmission (..), + encTransmissionNonce, FwdResponse (..), FwdTransmission (..), NameRecord (..), @@ -280,7 +281,7 @@ import Simplex.Messaging.ServiceScheme import Simplex.Messaging.SimplexName (LabelHash, SimplexDomain (..), SimplexTLD (..), fullDomainName, labelHash) import Simplex.Messaging.Transport import Simplex.Messaging.Transport.Client (TransportHost, TransportHosts (..)) -import Simplex.Messaging.Util (bshow, eitherToMaybe, safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Util (bshow, eitherToMaybe, packZipWith, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal @@ -702,6 +703,11 @@ instance Encoding NewNtfCreds where newtype EncTransmission = EncTransmission ByteString deriving (Show) +encTransmissionNonce :: VersionSMP -> C.CbNonce -> C.CbNonce +encTransmissionNonce v nonce@(C.CbNonce s) + | v >= fwdNoncesSMPVersion = C.cbNonce $ packZipWith xor (smpEncode v) s <> BS.drop 2 s + | otherwise = nonce + data FwdTransmission = FwdTransmission { fwdCorrId :: CorrId, fwdVersion :: VersionSMP, @@ -737,8 +743,8 @@ data BrokerMsg where NMSG :: C.CbNonce -> EncNMsgMeta -> BrokerMsg -- Should include certificate chain PKEY :: SessionId -> VersionRangeSMP -> CertChainPubKey -> BrokerMsg -- TLS-signed server key for proxy shared secret and initial sender key - RRES :: EncFwdResponse -> BrokerMsg -- relay to proxy - PRES :: EncResponse -> BrokerMsg -- proxy to client + RRES :: Maybe C.CbNonce -> EncFwdResponse -> BrokerMsg -- relay to proxy + PRES :: Maybe C.CbNonce -> EncResponse -> BrokerMsg -- proxy to client END :: BrokerMsg ENDS :: Int64 -> IdsHash -> BrokerMsg DELD :: BrokerMsg @@ -1985,8 +1991,8 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where NID nId srvNtfDh -> e (NID_, ' ', nId, srvNtfDh) NMSG nmsgNonce encNMsgMeta -> e (NMSG_, ' ', nmsgNonce, encNMsgMeta) PKEY sid vr certKey -> e (PKEY_, ' ', sid, vr, certKey) - RRES (EncFwdResponse encBlock) -> e (RRES_, ' ', Tail encBlock) - PRES (EncResponse encBlock) -> e (PRES_, ' ', Tail encBlock) + RRES nonce_ (EncFwdResponse encBlock) -> fwdResp RRES_ nonce_ encBlock + PRES nonce_ (EncResponse encBlock) -> fwdResp PRES_ nonce_ encBlock END -> e END_ ENDS n idsHash -> serviceResp ENDS_ n idsHash DELD -> e DELD_ @@ -2010,6 +2016,9 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where serviceResp tag n idsHash | v >= rcvServiceSMPVersion = e (tag, ' ', n, idsHash) | otherwise = e (tag, ' ', n) + fwdResp tag nonce_ encBlock + | v >= fwdNoncesSMPVersion = e (tag, ' ', nonce_, Tail encBlock) + | otherwise = e (tag, ' ', Tail encBlock) protocolP v = \case MSG_ -> do @@ -2041,8 +2050,8 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where NID_ -> NID <$> _smpP <*> smpP NMSG_ -> NMSG <$> _smpP <*> smpP PKEY_ -> PKEY <$> _smpP <*> smpP <*> smpP - RRES_ -> RRES <$> (EncFwdResponse . unTail <$> _smpP) - PRES_ -> PRES <$> (EncResponse . unTail <$> _smpP) + RRES_ -> fwdRespP RRES EncFwdResponse + PRES_ -> fwdRespP PRES EncResponse END_ -> pure END ENDS_ -> serviceRespP ENDS DELD_ -> pure DELD @@ -2059,6 +2068,9 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where serviceRespP resp | v >= rcvServiceSMPVersion = resp <$> _smpP <*> smpP | otherwise = resp <$> _smpP <*> pure mempty + fwdRespP :: (Maybe C.CbNonce -> a -> BrokerMsg) -> (ByteString -> a) -> Parser BrokerMsg + fwdRespP resp enc = resp <$> (A.space *> nonceP) <*> (enc <$> A.takeByteString) + nonceP = if v >= fwdNoncesSMPVersion then smpP else pure Nothing fromProtocolError = \case PECmdSyntax -> CMD SYNTAX @@ -2075,7 +2087,7 @@ instance ProtocolEncoding SMPVersion ErrorType BrokerMsg where -- PONG response must not have queue ID PONG -> noEntityMsg PKEY {} -> noEntityMsg - RRES _ -> noEntityMsg + RRES {} -> noEntityMsg ALLS -> noEntityMsg RNAME {} -> noEntityMsg -- other broker responses must have queue ID diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 405e9b5f0..7894a4eab 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -1462,7 +1462,7 @@ client inc own pRequests forkProxiedCmd $ do liftIO (runExceptT (forwardSMPTransmission smp corrId fwdV pubKey encBlock) `E.catches` clientHandlers) >>= \case - Right r -> PRES r <$ inc own pSuccesses + Right (nonce_, r) -> PRES nonce_ r <$ inc own pSuccesses Left e -> ERR (smpProxyError e) <$ case e of PCEProtocolError {} -> inc own pSuccesses _ -> inc own pErrorsOther @@ -2131,7 +2131,7 @@ client unless (fwdVersion `isCompatible` thServerVRange thParams') $ throwE $ transportErr TEVersion let clientSecret = C.dh' fwdKey serverPrivKey clientNonce = C.cbNonce $ bs fwdCorrId - b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret clientNonce et + b <- liftEitherWith (const CRYPTO) $ C.cbDecrypt clientSecret (encTransmissionNonce fwdVersion clientNonce) et let clntTHParams = smpTHParamsSetVersion fwdVersion thParams' -- only allowing single forwarded transactions t' <- case tParse clntTHParams b of @@ -2144,9 +2144,13 @@ client TBError _ _ : _ -> throwE BLOCK TBTransmission b' _ : _ -> pure b' TBTransmissions b' _ _ : _ -> pure b' - r2 <- liftEitherWith (const BLOCK) $ EncResponse <$> C.cbEncrypt clientSecret (C.reverseNonce clientNonce) r' paddedProxiedTLength + nonce_ <- + if fwdVersion >= fwdNoncesSMPVersion + then Just <$> (atomically . C.randomCbNonce =<< asks random) + else pure Nothing + r2 <- liftEitherWith (const BLOCK) $ EncResponse <$> C.cbEncrypt clientSecret (fromMaybe (C.reverseNonce clientNonce) nonce_) r' paddedProxiedTLength let fr = FwdResponse {fwdCorrId, fwdResponse = r2} - pure $ RRES $ EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) + pure $ RRES nonce_ $ EncFwdResponse $ C.cbEncryptNoPad sessSecret (C.reverseNonce proxyNonce) (smpEncode fr) -- the inner response, or Nothing if forked (RSLV). r_ <- lift (rejectOrVerify clntThAuth t') >>= \case -- rejectOrVerify filters allowed commands, no need to repeat it here. diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 6fa307fd1..0a082a388 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -54,6 +54,7 @@ module Simplex.Messaging.Transport namesSMPVersion, serverInfoSMPVersion, nameAvailSMPVersion, + fwdNoncesSMPVersion, simplexMQVersion, smpBlockSize, TransportConfig (..), @@ -177,6 +178,7 @@ smpBlockSize = 16384 -- 20 - public namespaces resolver, RSLV command (6/20/2026) -- 21 - server public information in handshake (7/5/2026) -- 22 - RNAME answers name availability as well as the record (7/25/2026) +-- 23 - version in forwarded command nonce, random nonce in forwarded responses (10/2/2026) data SMPVersion @@ -218,6 +220,9 @@ serverInfoSMPVersion = VersionSMP 21 nameAvailSMPVersion :: VersionSMP nameAvailSMPVersion = VersionSMP 22 +fwdNoncesSMPVersion :: VersionSMP +fwdNoncesSMPVersion = VersionSMP 23 + minClientSMPRelayVersion :: VersionSMP minClientSMPRelayVersion = VersionSMP 14 @@ -225,20 +230,20 @@ minServerSMPRelayVersion :: VersionSMP minServerSMPRelayVersion = VersionSMP 14 currentClientSMPRelayVersion :: VersionSMP -currentClientSMPRelayVersion = VersionSMP 22 +currentClientSMPRelayVersion = VersionSMP 23 currentServerSMPRelayVersion :: VersionSMP -currentServerSMPRelayVersion = VersionSMP 22 +currentServerSMPRelayVersion = VersionSMP 23 -- Max SMP protocol version to be used in e2e encrypted connection between -- client and server, as defined by SMP proxy. Normally set below the current -- version to prevent client version fingerprinting by the destination relays --- when clients upgrade at different times. Pinned to the current version (22) --- for this release because a proxied RSLV only carries availability from --- nameAvailSMPVersion (22), so the one-version anti-fingerprinting buffer does --- not apply yet; it reappears once the current version advances past 22. +-- when clients upgrade at different times. Pinned to the current version (23) +-- for this release because forwarded commands use the nonces from +-- fwdNoncesSMPVersion (23), so the one-version anti-fingerprinting buffer does +-- not apply yet; it reappears once the current version advances past 23. proxiedSMPRelayVersion :: VersionSMP -proxiedSMPRelayVersion = VersionSMP 22 +proxiedSMPRelayVersion = VersionSMP 23 -- minimal supported protocol version is 14 supportedClientSMPRelayVRange :: VersionRangeSMP diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index 319f885ea..8194e9ccd 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -231,7 +231,11 @@ pattern Rcvd' :: AgentMsgId -> AgentMsgId -> AEvent 'AEConn pattern Rcvd' aMsgId rcvdMsgId <- RCVD MsgMeta {integrity = MsgOk, recipient = (aMsgId, _)} [MsgReceipt {agentMsgId = rcvdMsgId, msgRcptStatus = MROk}] smpCfgVPrev :: ProtocolClientConfig SMPVersion -smpCfgVPrev = (smpCfg agentCfg) {serverVRange = prevRange $ serverVRange $ smpCfg agentCfg} +smpCfgVPrev = + (smpCfg agentCfg) + { serverVRange = prevRange $ serverVRange $ smpCfg agentCfg, + proxiedRelayVRange = prevRange $ proxiedRelayVRange $ smpCfg agentCfg + } -- ntfCfgVPrev :: ProtocolClientConfig NTFVersion -- ntfCfgVPrev = (ntfCfg agentCfg) {clientALPN = Nothing, serverVRange = V.mkVersionRange (VersionNTF 1) (VersionNTF 1)} diff --git a/tests/SMPClient.hs b/tests/SMPClient.hs index 43aa22a76..f1d5af1db 100644 --- a/tests/SMPClient.hs +++ b/tests/SMPClient.hs @@ -349,6 +349,16 @@ proxyCfgShortTimeout = nt = NetworkTimeout {backgroundTimeout = 4_000000, interactiveTimeout = 4_000000} in cfg' {smpAgentCfg = aCfg {smpCfg = cCfg {networkConfig = (networkConfig cCfg) {tcpConnectTimeout = nt}}}} +proxyCfgVPrev :: AStoreType -> AServerConfig +proxyCfgVPrev msType = + updateCfg (proxyCfgMS msType) $ \cfg' -> + let aCfg = smpAgentCfg cfg' + cCfg = smpCfg aCfg + in cfg' + { smpServerVRange = prevRange $ smpServerVRange cfg', + smpAgentCfg = aCfg {smpCfg = cCfg {serverVRange = prevRange $ serverVRange cCfg}} + } + withSmpServerStoreMsgLogOn :: HasCallStack => (ASrvTransport, AStoreType) -> ServiceName -> (HasCallStack => ThreadId -> IO a) -> IO a withSmpServerStoreMsgLogOn (t, msType) = withSmpServerConfigOn t $ updateCfg (cfgMS msType) $ \cfg' -> cfg' {storeNtfsFile = Just testStoreNtfsFile, serverStatsBackupFile = Just testServerStatsBackupFile} diff --git a/tests/SMPProxyTests.hs b/tests/SMPProxyTests.hs index 430d52304..91bae09a6 100644 --- a/tests/SMPProxyTests.hs +++ b/tests/SMPProxyTests.hs @@ -73,6 +73,8 @@ smpProxyTests = do xit "no SMP service at host/port" todo xit "bad SMP fingerprint" todo xit "batching proxy requests" todo + it "relay rejects forwarded command with changed version" $ \_ -> + testChangedFwdVersion describe "deliver message via SMP proxy" $ do let srv1 = SMPServer testHost testPort testKeyHash srv2 = SMPServer testHost2 testPort2 testKeyHash @@ -95,6 +97,11 @@ smpProxyTests = do deliverMessageViaProxy proxyServ relayServ C.SEd25519 msg1 msg2 it "max message size, X25519 keys" . twoServersFirstProxy $ deliverMessageViaProxy proxyServ relayServ C.SX25519 msg1 msg2 + describe "version compatibility" $ do + let deliver clientVR = deliverMessagesViaProxyVR clientVR srv1 srv2 C.SEd448 ["hello 1"] ["hello 2"] + it "prev client" . twoServersFirstProxy $ deliver (prevRange supportedClientSMPRelayVRange) + it "prev proxy" . twoServersPrevProxy $ deliver supportedClientSMPRelayVRange + it "prev relay" . twoServersPrevRelay $ deliver supportedClientSMPRelayVRange describe "stress test 1k" $ do let deliver n = deliverMessagesViaProxy srv1 srv2 C.SEd448 [] (map bshow [1 :: Int .. n]) it "1x1000" . twoServersFirstProxy $ deliver 1000 @@ -155,6 +162,9 @@ smpProxyTests = do twoServersFirstProxy test msType = twoServers_ (proxyCfgMS msType) (updateCfg (cfgMS msType) $ \cfg_ -> cfg_ {msgQueueQuota = 128, maxJournalMsgCount = 256}) test msType twoServersMoreConc test msType = twoServers_ (updateCfg (proxyCfgMS msType) $ \cfg_ -> cfg_ {serverClientConcurrency = 128}) (updateCfg (cfgMS msType) $ \cfg_ -> cfg_ {msgQueueQuota = 128, maxJournalMsgCount = 256}) test msType twoServersNoConc test msType = twoServers_ (updateCfg (proxyCfgMS msType) $ \cfg_ -> cfg_ {serverClientConcurrency = 1}) (updateCfg (cfgMS msType) $ \cfg_ -> cfg_ {msgQueueQuota = 128, maxJournalMsgCount = 256}) test msType + twoServersPrevProxy test msType = twoServers_ (proxyCfgVPrev msType) (cfgMS msType) test msType + twoServersPrevRelay test msType = twoServers_ (proxyCfgMS msType) (prevServerVRange $ cfgMS msType) test msType + prevServerVRange cfg' = updateCfg cfg' $ \cfg_ -> cfg_ {smpServerVRange = prevRange $ smpServerVRange cfg_} twoServers_ :: AServerConfig -> AServerConfig -> IO () -> AStoreType -> IO () twoServers_ cfg1 cfg2 runTest (ASType qsType _) = withSmpServerConfigOn (transport @TLS) cfg1 testPort $ \_ -> @@ -167,11 +177,14 @@ deliverMessageViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> SM deliverMessageViaProxy proxyServ relayServ alg msg msg' = deliverMessagesViaProxy proxyServ relayServ alg [msg] [msg'] deliverMessagesViaProxy :: (C.AlgorithmI a, C.AuthAlgorithm a) => SMPServer -> SMPServer -> C.SAlgorithm a -> [ByteString] -> [ByteString] -> IO () -deliverMessagesViaProxy proxyServ relayServ alg unsecuredMsgs securedMsgs = do +deliverMessagesViaProxy = deliverMessagesViaProxyVR $ mkVersionRange minServerSMPRelayVersion currentClientSMPRelayVersion + +deliverMessagesViaProxyVR :: (C.AlgorithmI a, C.AuthAlgorithm a) => VersionRangeSMP -> SMPServer -> SMPServer -> C.SAlgorithm a -> [ByteString] -> [ByteString] -> IO () +deliverMessagesViaProxyVR clientVR proxyServ relayServ alg unsecuredMsgs securedMsgs = do g <- C.newRandom -- set up proxy ts <- getCurrentTime - pc' <- getProtocolClient g NRMInteractive (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = mkVersionRange minServerSMPRelayVersion currentClientSMPRelayVersion} [] Nothing ts (\_ -> pure ()) + pc' <- getProtocolClient g NRMInteractive (1, proxyServ, Nothing) defaultSMPClientConfig {serverVRange = clientVR, proxiedRelayVRange = clientVR} [] Nothing ts (\_ -> pure ()) pc <- either (fail . show) pure pc' THAuthClient {} <- maybe (fail "getProtocolClient returned no thAuth") pure $ thAuth $ thParams pc -- set up relay @@ -448,6 +461,21 @@ requestRelaySession = testSMPClient_ "localhost" testPort supportedServerSMPRelayVRange Nothing $ \(th :: THandleSMP TLS 'TClient) -> (\(_, _, reply) -> reply) <$> sendRecv th (Nothing, "1", NoEntity, SMP.PRXY testSMPServer2 Nothing) +testChangedFwdVersion :: IO () +testChangedFwdVersion = + withSmpServerConfigOn (transport @TLS) cfg testPort $ \_ -> do + g <- C.newRandom + ts <- getCurrentTime + rc <- either (fail . show) pure =<< getProtocolClient g NRMInteractive (1, testSMPServer, Nothing) defaultSMPClientConfig [] Nothing ts (\_ -> pure ()) + THAuthClient {peerServerPubKey} <- maybe (fail "getProtocolClient returned no thAuth") pure $ thAuth $ thParams rc + (cmdPubKey, cmdPrivKey) <- atomically $ C.generateKeyPair g + nonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g + let v = currentClientSMPRelayVersion + et <- either (fail . show) (pure . SMP.EncTransmission) $ C.cbEncrypt (C.dh' peerServerPubKey cmdPrivKey) (SMP.encTransmissionNonce v nonce) "" SMP.paddedProxiedTLength + let forward fwdVersion = forwardSMPTransmission rc (SMP.CorrId corrId) fwdVersion cmdPubKey et + _ <- runExceptT' $ forward v + runExceptT (forward $ prevVersion v) `shouldReturn` Left (PCEProtocolError SMP.CRYPTO) + -- Shared "phase 2" of the reconnection tests: start a healthy relay, confirm it is reachable -- directly (PING, not via the proxy) so a proxy failure can only mean the proxy didn't reconnect, -- let any stored connection error expire, then require the proxy to establish the session (PKEY).