refactor: replace smpDecode with decodeHandshake for improved handshake processing

This commit is contained in:
Ed Asriyan
2026-08-16 18:09:15 +00:00
parent c377f2c1b1
commit 2db8c2c915
4 changed files with 41 additions and 8 deletions
+2 -2
View File
@@ -73,7 +73,7 @@ import Simplex.Messaging.Client
)
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.Lazy as LC
import Simplex.Messaging.Encoding (smpDecode, smpEncode)
import Simplex.Messaging.Encoding (smpEncode)
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol
( BasicAuth,
@@ -167,7 +167,7 @@ xftpClientHandshakeV1 serverVRange keyHash@(C.KeyHash kh) c@HTTP2Client {session
let helloReq = H.requestNoBody "POST" "/" []
HTTP2Response {respBody = HTTP2Body {bodyHead = shsBody}} <-
liftError' xftpClientError $ sendRequest c helloReq Nothing
liftTransportErr (TEHandshake PARSE) . smpDecode =<< liftTransportErr TEBadBlock (C.unPad shsBody)
liftTransportErr (TEHandshake PARSE) . decodeHandshake =<< liftTransportErr TEBadBlock (C.unPad shsBody)
processServerHandshake :: XFTPServerHandshake -> ExceptT XFTPClientError IO (VersionRangeXFTP, C.PublicKeyX25519)
processServerHandshake XFTPServerHandshake {xftpVersionRange, sessionId = serverSessId, authPubKey = serverAuth} = do
unless (sessionId == serverSessId) $ throwE $ PCETransportError TEBadSession
+2 -2
View File
@@ -191,7 +191,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
| B.null bodyHead -> pure Nothing
| sniUsed -> do
body <- liftHS $ C.unPad bodyHead
XFTPClientHello {webChallenge} <- liftHS $ first show (smpDecode body)
XFTPClientHello {webChallenge} <- liftHS $ first show (decodeHandshake body)
pure webChallenge
| otherwise -> throwE HANDSHAKE
rng <- asks random
@@ -212,7 +212,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira
processClientHandshake pk = do
unless (B.length bodyHead == xftpBlockSize) $ throwE HANDSHAKE
body <- liftHS $ C.unPad bodyHead
XFTPClientHandshake {xftpVersion = v, keyHash} <- liftHS $ smpDecode body
XFTPClientHandshake {xftpVersion = v, keyHash} <- liftHS $ decodeHandshake body
kh <- asks serverIdentity
unless (keyHash == kh) $ throwE HANDSHAKE
case compatibleVRange' xftpServerVRange v of
+5 -3
View File
@@ -20,6 +20,7 @@ module Simplex.FileTransfer.Transport
XFTPServerHandshake (..),
-- xftpServerHandshake,
XFTPClientHello (..),
decodeHandshake,
THandleXFTP,
THandleParamsXFTP,
VersionXFTP,
@@ -139,7 +140,6 @@ instance Encoding XFTPClientHello where
smpP = do
webChallenge <- smpP
forM_ webChallenge $ \challenge -> unless (B.length challenge == 32) $ fail "bad XFTPClientHello webChallenge"
Tail _compat <- smpP
pure XFTPClientHello {webChallenge}
instance Encoding XFTPClientHandshake where
@@ -147,7 +147,6 @@ instance Encoding XFTPClientHandshake where
smpEncode (xftpVersion, keyHash)
smpP = do
(xftpVersion, keyHash) <- smpP
Tail _compat <- smpP
pure XFTPClientHandshake {xftpVersion, keyHash}
instance Encoding XFTPServerHandshake where
@@ -156,9 +155,12 @@ instance Encoding XFTPServerHandshake where
smpP = do
(xftpVersionRange, sessionId, authPubKey) <- smpP
webIdentityProof <- optional $ C.decodeSignature <$?> smpP
Tail _compat <- smpP
pure XFTPServerHandshake {xftpVersionRange, sessionId, authPubKey, webIdentityProof}
-- Decodes handshake block ignoring any trailing bytes, to allow adding fields in future versions (as SMP handshake does).
decodeHandshake :: Encoding a => ByteString -> Either String a
decodeHandshake = A.parseOnly smpP
sendEncFile :: Handle -> (Builder -> IO ()) -> LC.SbState -> Word32 -> IO ()
sendEncFile h send = go
where
+32 -1
View File
@@ -32,7 +32,7 @@ import Simplex.FileTransfer.Client
import Simplex.FileTransfer.Description (kb)
import Simplex.FileTransfer.Protocol (FileInfo (..), XFTPFileId, xftpBlockSize)
import Simplex.FileTransfer.Server.Env (AFStoreType, XFTPServerConfig (..))
import Simplex.FileTransfer.Transport (XFTPClientHandshake (..), XFTPClientHello (..), XFTPErrorType (..), XFTPRcvChunkSpec (..), XFTPServerHandshake (..), pattern VersionXFTP)
import Simplex.FileTransfer.Transport (XFTPClientHandshake (..), XFTPClientHello (..), XFTPErrorType (..), XFTPRcvChunkSpec (..), XFTPServerHandshake (..), decodeHandshake, pattern VersionXFTP)
import Simplex.Messaging.Client (ProtocolClientError (..))
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.Lazy as LC
@@ -85,6 +85,7 @@ xftpServerTests =
it "should upload and receive file chunk through SNI-enabled server" testFileChunkDeliverySNI
it "should complete web handshake with challenge-response" testWebHandshake
it "should re-handshake on same connection with xftp-web-hello header" testWebReHandshake
it "should ignore trailing handshake bytes (forward compatibility)" testHandshakeIgnoresTrailingBytes
it "should return padded SESSION error for stale web session" testStaleWebSession
chSize :: Integral a => a
@@ -579,6 +580,36 @@ testWebReHandshake =
resp2b <- either (error . show) pure =<< HC.sendRequest h2 (H2.requestBuilder "POST" "/" [] $ byteString clientHsPadded) (Just 5000000)
B.length (bodyHead (HC.respBody resp2b)) `shouldBe` 0
-- Simulates a future protocol version appending extra fields to handshake messages:
-- both client and server must ignore the trailing bytes (as SMP handshake does).
testHandshakeIgnoresTrailingBytes :: Expectation
testHandshakeIgnoresTrailingBytes =
withXFTPServerSNI $ \_ -> do
Fingerprint fpWeb <- loadFileFingerprint "tests/fixtures/web_ca.crt"
Fingerprint fpXFTP <- loadFileFingerprint "tests/fixtures/ca.crt"
let webCaHash = C.KeyHash fpWeb
keyHash = C.KeyHash fpXFTP
cfg = defaultTransportClientConfig {clientALPN = Just ["h2"], useSNI = True}
extra = "\1\2\3\4\5" :: ByteString
runTLSTransportClient defaultSupportedParamsHTTPS Nothing cfg Nothing "localhost" xftpTestPort (Just webCaHash) $ \(tls :: TLS 'TClient) -> do
let h2cfg = HC.defaultHTTP2ClientConfig {HC.bodyHeadSize = 65536}
h2 <- either (error . show) pure =<< HC.attachHTTP2Client h2cfg (THDomainName "localhost") xftpTestPort mempty 65536 tls
g <- C.newRandom
challenge <- atomically $ C.randomBytes 32 g
helloBody <- either (error . show) pure $ C.pad (smpEncode (XFTPClientHello {webChallenge = Just challenge})) xftpBlockSize
let helloReq = H2.requestBuilder "POST" "/" [("xftp-web-hello", "1")] $ byteString helloBody
resp1 <- either (error . show) pure =<< HC.sendRequest h2 helloReq (Just 5000000)
serverHsDecoded <- either (error . show) pure $ C.unPad (bodyHead (HC.respBody resp1))
-- client decodes the real server handshake and the same handshake with extra trailing bytes to the same result
XFTPServerHandshake {sessionId} <- either error pure $ decodeHandshake serverHsDecoded
XFTPServerHandshake {sessionId = sessionId'} <- either error pure $ decodeHandshake (serverHsDecoded <> extra)
sessionId' `shouldBe` sessionId
-- server accepts a client handshake with extra trailing bytes
let clientHs = XFTPClientHandshake {xftpVersion = VersionXFTP 1, keyHash}
clientHsPadded <- either (error . show) pure $ C.pad (smpEncode clientHs <> extra) xftpBlockSize
resp2 <- either (error . show) pure =<< HC.sendRequest h2 (H2.requestBuilder "POST" "/" [] $ byteString clientHsPadded) (Just 5000000)
B.length (bodyHead (HC.respBody resp2)) `shouldBe` 0
testStaleWebSession :: Expectation
testStaleWebSession =
withXFTPServerSNI $ \_ -> do