diff --git a/simplexmq.cabal b/simplexmq.cabal index 8924843d7..694ee40b9 100644 --- a/simplexmq.cabal +++ b/simplexmq.cabal @@ -1,7 +1,7 @@ cabal-version: 3.0 name: simplexmq -version: 7.0.0.3 +version: 7.0.0.6 synopsis: SimpleXMQ message broker description: This package includes <./docs/Simplex-Messaging-Server.html server>, <./docs/Simplex-Messaging-Client.html client> and diff --git a/src/Simplex/FileTransfer/Client.hs b/src/Simplex/FileTransfer/Client.hs index 9d19d0492..10f1d3611 100644 --- a/src/Simplex/FileTransfer/Client.hs +++ b/src/Simplex/FileTransfer/Client.hs @@ -147,7 +147,7 @@ getXFTPClient transportSession@(_, srv, _) config@XFTPClientConfig {clientALPN, let HTTP2Client {sessionId, sessionALPN} = http2Client v = VersionXFTP 1 thServerVRange = versionToRange v - thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, encryptBlock = Nothing, batch = True, serviceAuth = False} + thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, encryptBlock = Nothing, batch = True, serviceAuth = False, serverInfo = Nothing} logDebug $ "Client negotiated handshake protocol: " <> tshow sessionALPN thParams@THandleParams {thVersion} <- case sessionALPN of Just alpn diff --git a/src/Simplex/FileTransfer/Server.hs b/src/Simplex/FileTransfer/Server.hs index 3cd19e7c1..99cbb1339 100644 --- a/src/Simplex/FileTransfer/Server.hs +++ b/src/Simplex/FileTransfer/Server.hs @@ -157,7 +157,7 @@ xftpServer cfg@XFTPServerConfig {xftpPort, transportConfig, inactiveClientExpira reqBody <- getHTTP2Body r xftpBlockSize let v = VersionXFTP 1 thServerVRange = versionToRange v - thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, encryptBlock = Nothing, batch = True, serviceAuth = False} + thParams0 = THandleParams {sessionId, blockSize = xftpBlockSize, thVersion = v, thServerVRange, thAuth = Nothing, implySessId = False, encryptBlock = Nothing, batch = True, serviceAuth = False, serverInfo = Nothing} req0 = XFTPTransportRequest {thParams = thParams0, request = r, reqBody, sendResponse, sniUsed, addCORS = addCORS'} flip runReaderT env $ case sessionALPN of Nothing -> processRequest req0 diff --git a/src/Simplex/Messaging/Agent.hs b/src/Simplex/Messaging/Agent.hs index 34e90f127..987d7d725 100644 --- a/src/Simplex/Messaging/Agent.hs +++ b/src/Simplex/Messaging/Agent.hs @@ -49,6 +49,7 @@ module Simplex.Messaging.Agent deleteUser, setUserService, connRequestPQSupport, + prepareConnectionToCreate, createConnectionAsync, setConnShortLinkAsync, getConnShortLinkAsync, @@ -199,6 +200,7 @@ import qualified Simplex.Messaging.Agent.Store.DB as DB import Simplex.Messaging.Agent.Store.Entity import Simplex.Messaging.Agent.Store.Interface (closeDBStore, execSQL, getCurrentMigrations) import Simplex.Messaging.Agent.Store.Shared (UpMigration (..), upMigration) +import Simplex.Messaging.Server.Information (ServerPublicInfo) import qualified Simplex.Messaging.Agent.TSessionSubs as SS import Simplex.Messaging.Client (NetworkRequestMode (..), ProtocolClientError (..), SMPClientError, ServerTransmission (..), ServerTransmissionBatch, TransportSessionMode (..), nonBlockingWriteTBQueue, smpErrorClientNotice, temporaryClientError, unexpectedResponse) import qualified Simplex.Messaging.Crypto as C @@ -358,9 +360,14 @@ setUserService :: AgentClient -> UserId -> Bool -> AE () setUserService c = withAgentEnv c .: setUserService' c {-# INLINE setUserService #-} --- | Create SMP agent connection (NEW command) asynchronously, synchronous response is new connection id -createConnectionAsync :: ConnectionModeI c => AgentClient -> UserId -> ACorrId -> Bool -> SConnectionMode c -> CR.InitialKeys -> SubscriptionMode -> AE ConnId -createConnectionAsync c userId aCorrId enableNtfs = withAgentEnv c .:. newConnAsync c userId aCorrId enableNtfs +-- | Create SMP agent connection without queue (to be used with createConnectionAsync). +prepareConnectionToCreate :: AgentClient -> UserId -> Bool -> SConnectionMode c -> PQSupport -> AE ConnId +prepareConnectionToCreate c userId enableNtfs = withAgentEnv c .: newConnNoQueues c userId enableNtfs +{-# INLINE prepareConnectionToCreate #-} + +-- | Enqueue NEW command for a prepared connection. +createConnectionAsync :: ConnectionModeI c => AgentClient -> ACorrId -> ConnId -> Bool -> SConnectionMode c -> CR.InitialKeys -> SubscriptionMode -> AE () +createConnectionAsync c aCorrId connId enableNtfs = withAgentEnv c .:. newConnAsync c aCorrId connId enableNtfs {-# INLINE createConnectionAsync #-} -- | Create or update user's contact connection short link (LSET command) asynchronously, no synchronous response @@ -373,10 +380,9 @@ getConnShortLinkAsync :: AgentClient -> UserId -> ACorrId -> Maybe ConnId -> Con getConnShortLinkAsync c = withAgentEnv c .:: getConnShortLinkAsync' c {-# INLINE getConnShortLinkAsync #-} --- | Join SMP agent connection (JOIN command) asynchronously, synchronous response is new connection id. --- If connId is provided (for contact URIs), it updates the existing connection record created by getConnShortLinkAsync. -joinConnectionAsync :: AgentClient -> UserId -> ACorrId -> Maybe ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AE ConnId -joinConnectionAsync c userId aCorrId connId_ enableNtfs = withAgentEnv c .:: joinConnAsync c userId aCorrId connId_ enableNtfs +-- | Enqueue JOIN command for a prepared connection. +joinConnectionAsync :: AgentClient -> ACorrId -> Bool -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AE () +joinConnectionAsync c aCorrId updateConn connId enableNtfs = withAgentEnv c .:: joinConnAsync c aCorrId updateConn connId enableNtfs {-# INLINE joinConnectionAsync #-} -- | Allow connection to continue after CONF notification (LET command), no synchronous response @@ -384,9 +390,9 @@ allowConnectionAsync :: AgentClient -> ACorrId -> ConnId -> ConfirmationId -> Co allowConnectionAsync c = withAgentEnv c .:: allowConnectionAsync' c {-# INLINE allowConnectionAsync #-} --- | Accept contact after REQ notification (ACPT command) asynchronously, synchronous response is new connection id -acceptContactAsync :: AgentClient -> UserId -> ACorrId -> Bool -> ConfirmationId -> ConnInfo -> PQSupport -> SubscriptionMode -> AE ConnId -acceptContactAsync c userId aCorrId enableNtfs = withAgentEnv c .:: acceptContactAsync' c userId aCorrId enableNtfs +-- | Accept contact after REQ notification (ACPT command) asynchronously, for a prepared connection. +acceptContactAsync :: AgentClient -> ACorrId -> ConnId -> Bool -> ConfirmationId -> ConnInfo -> PQSupport -> SubscriptionMode -> AE () +acceptContactAsync c aCorrId connId enableNtfs = withAgentEnv c .:: acceptContactAsync' c aCorrId connId enableNtfs {-# INLINE acceptContactAsync #-} -- | Acknowledge message (ACK command) asynchronously, no synchronous response @@ -418,8 +424,9 @@ createConnection c nm userId enableNtfs checkNotices = withAgentEnv c .::. newCo -- Caller provides root signing key pair and link entity ID. -- Returns the created link and internal params. -- The link address is fully determined at this point. -prepareConnectionLink :: AgentClient -> UserId -> C.KeyPairEd25519 -> ByteString -> Bool -> Maybe CRClientData -> AE (CreatedConnLink 'CMContact, PreparedLinkParams) -prepareConnectionLink c userId rootKey linkEntityId checkNotices = withAgentEnv c . prepareConnectionLink' c userId rootKey linkEntityId checkNotices +prepareConnectionLink :: AgentClient -> UserId -> C.KeyPairEd25519 -> ByteString -> Bool -> Maybe CRClientData -> Maybe SMPServerWithAuth -> AE (CreatedConnLink 'CMContact, PreparedLinkParams) +prepareConnectionLink c userId rootKey linkEntityId checkNotices clientData srv_ = + withAgentEnv c $ prepareConnectionLink' c userId rootKey linkEntityId checkNotices clientData srv_ {-# INLINE prepareConnectionLink #-} -- | Create connection for prepared link (single network call). @@ -634,11 +641,11 @@ getConnectionRatchetAdHash c = withAgentEnv c . getConnectionRatchetAdHash' c {-# INLINE getConnectionRatchetAdHash #-} -- | Test protocol server -testProtocolServer :: forall p. ProtocolTypeI p => AgentClient -> NetworkRequestMode -> UserId -> ProtoServerWithAuth p -> IO (Maybe ProtocolTestFailure) +testProtocolServer :: forall p. ProtocolTypeI p => AgentClient -> NetworkRequestMode -> UserId -> ProtoServerWithAuth p -> IO (Either ProtocolTestFailure (Maybe (Either String ServerPublicInfo))) testProtocolServer c nm userId srv = withAgentEnv' c $ case protocolTypeI @p of SPSMP -> runSMPServerTest c nm userId srv - SPXFTP -> runXFTPServerTest c nm userId srv - SPNTF -> runNTFServerTest c nm userId srv + SPXFTP -> maybe (Right Nothing) Left <$> runXFTPServerTest c nm userId srv + SPNTF -> maybe (Right Nothing) Left <$> runNTFServerTest c nm userId srv -- | set SOCKS5 proxy on/off and optionally set TCP timeouts for fast network setNetworkConfig :: AgentClient -> NetworkConfig -> AE () @@ -846,11 +853,10 @@ setUserService' c userId enable = do unless ok $ throwE $ CMD PROHIBITED "setUserService" when (changed && not enable) $ withStore' c (`deleteClientServices` userId) -newConnAsync :: ConnectionModeI c => AgentClient -> UserId -> ACorrId -> Bool -> SConnectionMode c -> CR.InitialKeys -> SubscriptionMode -> AM ConnId -newConnAsync c userId corrId enableNtfs cMode pqInitKeys subMode = do - connId <- newConnNoQueues c userId enableNtfs cMode (CR.connPQEncryption pqInitKeys) +newConnAsync :: ConnectionModeI c => AgentClient -> ACorrId -> ConnId -> Bool -> SConnectionMode c -> CR.InitialKeys -> SubscriptionMode -> AM () +newConnAsync c corrId connId enableNtfs cMode pqInitKeys subMode = enqueueCommand c corrId connId Nothing $ AClientCommand $ NEW enableNtfs (ACM cMode) pqInitKeys subMode - pure connId +{-# INLINE newConnAsync #-} newConnNoQueues :: AgentClient -> UserId -> Bool -> SConnectionMode c -> PQSupport -> AM ConnId newConnNoQueues c userId enableNtfs cMode pqSupport = do @@ -861,34 +867,21 @@ newConnNoQueues c userId enableNtfs cMode pqSupport = do -- TODO [short links] TBC, but probably we will need async join for contact addresses as the contact will be created after user confirming the connection, -- and join should retry, the same as 1-time invitation joins. -joinConnAsync :: AgentClient -> UserId -> ACorrId -> Maybe ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId -joinConnAsync c userId corrId connId_ enableNtfs cReqUri@CRInvitationUri {} cInfo pqSup subMode = do - when (isJust connId_) $ throwE $ CMD PROHIBITED "joinConnAsync: connId not allowed for invitation URI" - withInvLock c (strEncode cReqUri) "joinConnAsync" $ do +joinConnAsync :: AgentClient -> ACorrId -> Bool -> ConnId -> Bool -> ConnectionRequestUri c -> ConnInfo -> PQSupport -> SubscriptionMode -> AM () +joinConnAsync c corrId updateConn connId enableNtfs cReqUri@CRInvitationUri {} cInfo pqSup subMode = do + when updateConn $ throwE $ CMD PROHIBITED "joinConnAsync: updateConn not allowed for invitation URI" + withInvLock c (strEncode cReqUri) "joinConnAsync" $ lift (compatibleInvitationUri cReqUri) >>= \case Just (_, Compatible (CR.E2ERatchetParams v _ _ _), Compatible connAgentVersion) -> do - g <- asks random let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion (Just v) - cData = ConnData {userId, connId = "", connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport} - connId <- withStore c $ \db -> createNewConn db g cData SCMInvitation enqueueCommand c corrId connId Nothing $ AClientCommand $ JOIN enableNtfs (ACR sConnectionMode cReqUri) pqSupport subMode cInfo - pure connId Nothing -> throwE $ AGENT A_VERSION -joinConnAsync c userId corrId connId_ enableNtfs cReqUri@(CRContactUri _) cInfo pqSup subMode = do +joinConnAsync c corrId updateConn connId enableNtfs cReqUri@(CRContactUri _) cInfo pqSup subMode = lift (compatibleContactUri cReqUri) >>= \case Just (_, Compatible connAgentVersion) -> do let pqSupport = pqSup `CR.pqSupportAnd` versionPQSupport_ connAgentVersion Nothing - connId <- case connId_ of - Just cId -> do - -- update connection record created by getConnShortLinkAsync - withStore' c $ \db -> updateNewConnJoin db cId connAgentVersion pqSupport enableNtfs - pure cId - Nothing -> do - g <- asks random - let cData = ConnData {userId, connId = "", connAgentVersion, enableNtfs, lastExternalSndId = 0, deleted = False, ratchetSyncState = RSOk, pqSupport} - withStore c $ \db -> createNewConn db g cData SCMInvitation + when updateConn $ withStore' c $ \db -> updateNewConnJoin db connId connAgentVersion pqSupport enableNtfs enqueueCommand c corrId connId Nothing $ AClientCommand $ JOIN enableNtfs (ACR sConnectionMode cReqUri) pqSupport subMode cInfo - pure connId Nothing -> throwE $ AGENT A_VERSION allowConnectionAsync' :: AgentClient -> ACorrId -> ConnId -> ConfirmationId -> ConnInfo -> AM () @@ -904,11 +897,11 @@ allowConnectionAsync' c corrId connId confId ownConnInfo = -- and also it can't be triggered by user concurrently several times in a row. It could be improved similarly to -- `acceptContact` by creating a new map for invitation locks and taking lock here, and removing `unacceptInvitation` -- while marking invitation as accepted inside "lock level transaction" after successful `joinConnAsync`. -acceptContactAsync' :: AgentClient -> UserId -> ACorrId -> Bool -> InvitationId -> ConnInfo -> PQSupport -> SubscriptionMode -> AM ConnId -acceptContactAsync' c userId corrId enableNtfs invId ownConnInfo pqSupport subMode = do +acceptContactAsync' :: AgentClient -> ACorrId -> ConnId -> Bool -> InvitationId -> ConnInfo -> PQSupport -> SubscriptionMode -> AM () +acceptContactAsync' c corrId connId enableNtfs invId ownConnInfo pqSupport subMode = do Invitation {connReq} <- withStore c $ \db -> getInvitation db "acceptContactAsync'" invId withStore' c $ \db -> acceptInvitation db invId ownConnInfo - joinConnAsync c userId corrId Nothing enableNtfs connReq ownConnInfo pqSupport subMode `catchAllErrors` \err -> do + joinConnAsync c corrId False connId enableNtfs connReq ownConnInfo pqSupport subMode `catchAllErrors` \err -> do withStore' c (`unacceptInvitation` invId) throwE err @@ -974,10 +967,10 @@ newConn c nm userId enableNtfs checkNotices cMode linkData_ clientData pqInitKey -- | Prepare connection link for contact mode (no network, no database). -- Caller provides root signing key pair and link entity ID. -prepareConnectionLink' :: AgentClient -> UserId -> C.KeyPairEd25519 -> ByteString -> Bool -> Maybe CRClientData -> AM (CreatedConnLink 'CMContact, PreparedLinkParams) -prepareConnectionLink' c userId rootKey@(_, plpRootPrivKey) linkEntityId checkNotices clientData = do +prepareConnectionLink' :: AgentClient -> UserId -> C.KeyPairEd25519 -> ByteString -> Bool -> Maybe CRClientData -> Maybe SMPServerWithAuth -> AM (CreatedConnLink 'CMContact, PreparedLinkParams) +prepareConnectionLink' c userId rootKey@(_, plpRootPrivKey) linkEntityId checkNotices clientData srv_ = do g <- asks random - plpSrvWithAuth@(ProtoServerWithAuth srv _) <- getSMPServer c userId + plpSrvWithAuth@(ProtoServerWithAuth srv _) <- maybe (getSMPServer c userId) pure srv_ when checkNotices $ checkClientNotices c plpSrvWithAuth AgentConfig {smpClientVRange, smpAgentVRange} <- asks config plpNonce@(C.CbNonce corrId) <- atomically $ C.randomCbNonce g @@ -1233,8 +1226,8 @@ newRcvConnSrv c nm userId connId enableNtfs cMode userLinkData_ clientData pqIni SCMInvitation -> do g <- asks random let pqEnc = CR.initialPQEncryption (isJust userLinkData_) pqInitKeys - (pk1, pk2, pKem, e2eRcvParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eEncryptVRange) pqEnc - withStore' c $ \db -> createRatchetX3dhKeys db connId pk1 pk2 pKem + (pks, e2eRcvParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eEncryptVRange) pqEnc + withStore' c $ \db -> createRatchetX3dhKeys db connId pks pure $ CRInvitationUri crData $ toVersionRangeT e2eRcvParams e2eEncryptVRange prepareLinkData :: UserConnLinkData c -> C.PublicKeyX25519 -> AM (C.CbNonce, SMPQueueUri, ConnectionRequestUri c, ClntQueueReqData) prepareLinkData userLinkData e2eDhKey = do @@ -1355,9 +1348,9 @@ startJoinInvitation c userId connId sq_ enableNtfs cReqUri pqSup = Nothing -> throwE $ AGENT A_VERSION where createRatchet_ db g maxSupported pqSupport e2eRcvParams@(CR.E2ERatchetParams v _ rcDHRr kem_) = do - (pk1, pk2, pKem, e2eSndParams) <- liftIO $ CR.generateSndE2EParams g v (CR.replyKEM_ v kem_ pqSupport) + (pks, e2eSndParams) <- liftIO $ CR.generateSndE2EParams g v (CR.replyKEM_ v kem_ pqSupport) (_, rcDHRs) <- atomically $ C.generateKeyPair g - rcParams <- liftEitherWith (SEAgentError . cryptoError) $ CR.pqX3dhSnd pk1 pk2 pKem e2eRcvParams + rcParams <- liftEitherWith (SEAgentError . cryptoError) $ CR.pqX3dhSnd pks e2eRcvParams let rcVs = CR.RatchetVersions {current = v, maxSupported} rc = CR.initSndRatchet rcVs rcDHRr rcDHRs rcParams liftIO $ createSndRatchet db connId rc e2eSndParams @@ -1434,8 +1427,8 @@ joinConnSrv c nm userId connId enableNtfs cReqUri@CRContactUri {} cInfo pqSup su Left e -> do nonBlockingWriteTBQueue (subQ c) ("", connId, AEvt SAEConn (ERR $ INTERNAL $ "no rcv ratchet " <> show e)) let pqEnc = CR.initialPQEncryption False pqInitKeys - (pk1, pk2, pKem, e2eRcvParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eVR) pqEnc - createRatchetX3dhKeys db connId pk1 pk2 pKem + (pks, e2eRcvParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eVR) pqEnc + createRatchetX3dhKeys db connId pks pure e2eRcvParams let cReq = CRInvitationUri crData $ toVersionRangeT e2eRcvParams e2eVR pure $ CCLink cReq Nothing @@ -2483,11 +2476,11 @@ synchronizeRatchet' c connId pqSupport' force = withConnLock c connId "synchroni let cData' = cData {pqSupport = pqSupport'} :: ConnData AgentConfig {e2eEncryptVRange} <- asks config g <- asks random - (pk1, pk2, pKem, e2eParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eEncryptVRange) pqSupport' + (pks, e2eParams) <- liftIO $ CR.generateRcvE2EParams g (maxVersion e2eEncryptVRange) pqSupport' enqueueRatchetKeyMsgs c sqs e2eParams withStore' c $ \db -> do setConnRatchetSync db connId RSStarted - setRatchetX3dhKeys db connId pk1 pk2 pKem + setRatchetX3dhKeys db connId pks let cData'' = cData' {ratchetSyncState = RSStarted} :: ConnData conn' = DuplexConnection cData'' rqs sqs connectionStats c conn' @@ -3418,8 +3411,8 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar -- party initiating connection (RcvConnection _ _, Just (CR.AE2ERatchetParams _ e2eSndParams@(CR.E2ERatchetParams e2eVersion _ _ _))) -> do unless (e2eVersion `isCompatible` e2eEncryptVRange) (throwE $ AGENT A_VERSION) - (pk1, rcDHRs, pKem) <- withStore c (`getRatchetX3dhKeys` connId) - rcParams <- liftError cryptoError $ CR.pqX3dhRcv pk1 rcDHRs pKem e2eSndParams + pks@(_, rcDHRs, _) <- withStore c (`getRatchetX3dhKeys` connId) + rcParams <- liftError cryptoError $ CR.pqX3dhRcv pks e2eSndParams let rcVs = CR.RatchetVersions {current = e2eVersion, maxSupported = maxVersion e2eEncryptVRange} pqSupport' = pqSupport `CR.pqSupportAnd` versionPQSupport_ agentVersion (Just e2eVersion) rc = CR.initRcvRatchet rcVs rcDHRs rcParams pqSupport' @@ -3660,7 +3653,7 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar exists <- checkRatchetKeyHashExists db connId rkHashRcv unless exists $ addProcessedRatchetKeyHash db connId rkHashRcv pure exists - getSendRatchetKeys :: AM (C.PrivateKeyX448, C.PrivateKeyX448, Maybe CR.RcvPrivRKEMParams) + getSendRatchetKeys :: AM (CR.RcvE2EPrivRatchetParams 'C.X448) getSendRatchetKeys = case rss of RSOk -> sendReplyKey -- receiving client RSAllowed -> sendReplyKey @@ -3676,9 +3669,9 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar where sendReplyKey = do g <- asks random - (pk1, pk2, pKem, e2eParams) <- liftIO $ CR.generateRcvE2EParams g e2eVersion pqSupport + (pks, e2eParams) <- liftIO $ CR.generateRcvE2EParams g e2eVersion pqSupport enqueueRatchetKeyMsgs c sqs e2eParams - pure (pk1, pk2, pKem) + pure pks notifyRatchetSyncError = do let cData'' = cData' {ratchetSyncState = RSRequired} :: ConnData conn'' = updateConnection cData'' conn' @@ -3697,14 +3690,14 @@ processSMPTransmissions c@AgentClient {subQ} (tSess@(userId, srv, _), THandlePar createRatchet db connId rc -- compare public keys `k1` in AgentRatchetKey messages sent by self and other party -- to determine ratchet initilization ordering - initRatchet :: CR.RatchetVersions -> (C.PrivateKeyX448, C.PrivateKeyX448, Maybe CR.RcvPrivRKEMParams) -> AM () + initRatchet :: CR.RatchetVersions -> CR.RcvE2EPrivRatchetParams 'C.X448 -> AM () initRatchet rcVs (pk1, pk2, pKem) | rkHash (C.publicKey pk1) (C.publicKey pk2) <= rkHashRcv = do - rcParams <- liftError cryptoError $ CR.pqX3dhRcv pk1 pk2 pKem e2eOtherPartyParams + rcParams <- liftError cryptoError $ CR.pqX3dhRcv (pk1, pk2, pKem) e2eOtherPartyParams recreateRatchet $ CR.initRcvRatchet rcVs pk2 rcParams pqSupport | otherwise = do (_, rcDHRs) <- atomically . C.generateKeyPair =<< asks random - rcParams <- liftEitherWith cryptoError $ CR.pqX3dhSnd pk1 pk2 (CR.APRKP CR.SRKSProposed <$> pKem) e2eOtherPartyParams + rcParams <- liftEitherWith cryptoError $ CR.pqX3dhSnd (pk1, pk2, CR.APRKP CR.SRKSProposed <$> pKem) e2eOtherPartyParams recreateRatchet $ CR.initSndRatchet rcVs k2Rcv rcDHRs rcParams void . enqueueMessages' c cData' sqs SMP.MsgFlags {notification = True} $ EREADY lastExternalSndId diff --git a/src/Simplex/Messaging/Agent/Client.hs b/src/Simplex/Messaging/Agent/Client.hs index bb4f1a950..c4e5047c7 100644 --- a/src/Simplex/Messaging/Agent/Client.hs +++ b/src/Simplex/Messaging/Agent/Client.hs @@ -304,11 +304,12 @@ import Simplex.Messaging.Protocol import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Protocol.Types import Simplex.Messaging.Server.QueueStore.QueueInfo +import Simplex.Messaging.Server.Information (ServerPublicInfo) import Simplex.Messaging.Session import Simplex.Messaging.SystemTime import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM -import Simplex.Messaging.Transport (HandshakeError (..), SMPServiceRole (..), SMPVersion, ServiceCredentials (..), SessionId, THClientService' (..), THandleAuth (..), THandleParams (sessionId, thAuth, thVersion), TransportError (..), TransportPeer (..), sndAuthKeySMPVersion, shortLinksSMPVersion, newNtfCredsSMPVersion) +import Simplex.Messaging.Transport (HandshakeError (..), SMPServiceRole (..), SMPVersion, ServiceCredentials (..), SessionId, THClientService' (..), THandleAuth (..), THandleParams (sessionId, thAuth, thVersion, serverInfo), TransportError (..), TransportPeer (..), sndAuthKeySMPVersion, shortLinksSMPVersion, newNtfCredsSMPVersion) import Simplex.Messaging.Transport.Client (TransportHost (..)) import Simplex.Messaging.Transport.Credentials import Simplex.Messaging.Util @@ -1281,7 +1282,7 @@ data ProtocolTestFailure = ProtocolTestFailure } deriving (Eq, Show) -runSMPServerTest :: AgentClient -> NetworkRequestMode -> UserId -> SMPServerWithAuth -> AM' (Maybe ProtocolTestFailure) +runSMPServerTest :: AgentClient -> NetworkRequestMode -> UserId -> SMPServerWithAuth -> AM' (Either ProtocolTestFailure (Maybe (Either String ServerPublicInfo))) runSMPServerTest c@AgentClient {presetDomains} nm userId (ProtoServerWithAuth srv auth) = do cfg <- getClientConfig c smpCfg C.AuthAlg ra <- asks $ rcvAuthAlg . config @@ -1292,6 +1293,9 @@ runSMPServerTest c@AgentClient {presetDomains} nm userId (ProtoServerWithAuth sr ts <- readTVarIO $ proxySessTs c getProtocolClient g nm tSess cfg presetDomains Nothing ts (\_ -> pure ()) >>= \case Right smp -> do + -- let serverInfo = case serverInfoBytes (thParams smp) of + -- Nothing -> Left "No server info received" + -- Just bs -> J.eitherDecodeStrict bs rKeys@(_, rpKey) <- atomically $ C.generateAuthKeyPair ra g (sKey, spKey) <- atomically $ C.generateAuthKeyPair sa g (dhKey, _) <- atomically $ C.generateKeyPair g @@ -1303,8 +1307,11 @@ runSMPServerTest c@AgentClient {presetDomains} nm userId (ProtoServerWithAuth sr _ -> secureSMPQueue smp nm rpKey rcvId sKey liftError (testErr TSDeleteQueue) $ deleteSMPQueue smp nm rpKey rcvId ok <- netTimeoutInt (tcpTimeout $ networkConfig cfg) nm `timeout` closeProtocolClient smp - pure $ either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok - Left e -> pure (Just $ testErr TSConnect e) + pure $ r >> maybe (Left (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const $ Right $ serverInfo (thParams smp)) ok + -- pure $ case either Just (const Nothing) r <|> maybe (Just (ProtocolTestFailure TSDisconnect $ BROKER addr TIMEOUT)) (const Nothing) ok of + -- Just failErr -> Left failErr + -- Nothing -> Right serverInfo + Left e -> pure $ Left (testErr TSConnect e) where addr = B.unpack $ strEncode srv testErr :: ProtocolTestStep -> SMPClientError -> ProtocolTestFailure diff --git a/src/Simplex/Messaging/Agent/Store/AgentStore.hs b/src/Simplex/Messaging/Agent/Store/AgentStore.hs index e3ea1671b..918e320e6 100644 --- a/src/Simplex/Messaging/Agent/Store/AgentStore.hs +++ b/src/Simplex/Messaging/Agent/Store/AgentStore.hs @@ -1359,11 +1359,11 @@ deleteSndMsgsExpired db ttl limit = do |] (cutoffTs, limit) -createRatchetX3dhKeys :: DB.Connection -> ConnId -> C.PrivateKeyX448 -> C.PrivateKeyX448 -> Maybe CR.RcvPrivRKEMParams -> IO () -createRatchetX3dhKeys db connId x3dhPrivKey1 x3dhPrivKey2 pqPrivKem = +createRatchetX3dhKeys :: DB.Connection -> ConnId -> CR.RcvE2EPrivRatchetParams 'C.X448 -> IO () +createRatchetX3dhKeys db connId (x3dhPrivKey1, x3dhPrivKey2, pqPrivKem) = DB.execute db "INSERT INTO ratchets (conn_id, x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem) VALUES (?, ?, ?, ?)" (connId, x3dhPrivKey1, x3dhPrivKey2, pqPrivKem) -getRatchetX3dhKeys :: DB.Connection -> ConnId -> IO (Either StoreError (C.PrivateKeyX448, C.PrivateKeyX448, Maybe CR.RcvPrivRKEMParams)) +getRatchetX3dhKeys :: DB.Connection -> ConnId -> IO (Either StoreError (CR.RcvE2EPrivRatchetParams 'C.X448)) getRatchetX3dhKeys db connId = firstRow' keys SEX3dhKeysNotFound $ DB.query db "SELECT x3dh_priv_key_1, x3dh_priv_key_2, pq_priv_kem FROM ratchets WHERE conn_id = ?" (Only connId) @@ -1373,8 +1373,8 @@ getRatchetX3dhKeys db connId = _ -> Left SEX3dhKeysNotFound -- used to remember new keys when starting ratchet re-synchronization -setRatchetX3dhKeys :: DB.Connection -> ConnId -> C.PrivateKeyX448 -> C.PrivateKeyX448 -> Maybe CR.RcvPrivRKEMParams -> IO () -setRatchetX3dhKeys db connId x3dhPrivKey1 x3dhPrivKey2 pqPrivKem = +setRatchetX3dhKeys :: DB.Connection -> ConnId -> CR.RcvE2EPrivRatchetParams 'C.X448 -> IO () +setRatchetX3dhKeys db connId (x3dhPrivKey1, x3dhPrivKey2, pqPrivKem) = DB.execute db [sql| diff --git a/src/Simplex/Messaging/Client.hs b/src/Simplex/Messaging/Client.hs index dc811fa0b..7dba8613c 100644 --- a/src/Simplex/Messaging/Client.hs +++ b/src/Simplex/Messaging/Client.hs @@ -230,7 +230,8 @@ smpClientStub g sessionId thVersion thAuth = do implySessId = thVersion >= authCmdsSMPVersion, encryptBlock = Nothing, batch = True, - serviceAuth = thVersion >= serviceCertsSMPVersion + serviceAuth = thVersion >= serviceCertsSMPVersion, + serverInfo = Nothing }, sessionTs = ts, client_ = diff --git a/src/Simplex/Messaging/Crypto/Ratchet.hs b/src/Simplex/Messaging/Crypto/Ratchet.hs index 7250a1d60..2d3a5a205 100644 --- a/src/Simplex/Messaging/Crypto/Ratchet.hs +++ b/src/Simplex/Messaging/Crypto/Ratchet.hs @@ -45,6 +45,7 @@ module Simplex.Messaging.Crypto.Ratchet AE2ERatchetParams (..), E2ERatchetParamsUri (..), E2ERatchetParams (..), + RcvE2EPrivRatchetParams, VersionE2E, VersionRangeE2E, pattern VersionE2E, @@ -403,24 +404,30 @@ instance RatchetKEMStateI s => ToField (PrivRKEMParams s) where toField = toFiel instance (Typeable s, RatchetKEMStateI s) => FromField (PrivRKEMParams s) where fromField = blobFieldDecoder smpDecode +type E2EPrivRatchetParams s a = (PrivateKey a, PrivateKey a, Maybe (PrivRKEMParams s))\ + +type RcvE2EPrivRatchetParams a = E2EPrivRatchetParams 'RKSProposed a + +type AE2EPrivRatchetParams a = (PrivateKey a, PrivateKey a, Maybe APrivRKEMParams) + data UseKEM (s :: RatchetKEMState) where ProposeKEM :: UseKEM 'RKSProposed AcceptKEM :: KEMPublicKey -> UseKEM 'RKSAccepted data AUseKEM = forall s. RatchetKEMStateI s => AUseKEM (SRatchetKEMState s) (UseKEM s) -mkRcvE2ERatchetParams :: VersionE2E -> (PrivateKey a, PrivateKey a, Maybe RcvPrivRKEMParams) -> RcvE2ERatchetParams a +mkRcvE2ERatchetParams :: VersionE2E -> RcvE2EPrivRatchetParams a -> RcvE2ERatchetParams a mkRcvE2ERatchetParams v (pk1, pk2, pKem) = E2ERatchetParams v (publicKey pk1) (publicKey pk2) (mkKem <$> pKem) where mkKem :: RcvPrivRKEMParams -> RcvRKEMParams mkKem (PrivateRKParamsProposed (k, _)) = RKParamsProposed k -generateE2EParams :: forall s a. (AlgorithmI a, DhAlgorithm a) => TVar ChaChaDRG -> VersionE2E -> Maybe (UseKEM s) -> IO (PrivateKey a, PrivateKey a, Maybe (PrivRKEMParams s), E2ERatchetParams s a) +generateE2EParams :: forall s a. (AlgorithmI a, DhAlgorithm a) => TVar ChaChaDRG -> VersionE2E -> Maybe (UseKEM s) -> IO (E2EPrivRatchetParams s a, E2ERatchetParams s a) generateE2EParams g v useKEM_ = do (k1, pk1) <- atomically $ generateKeyPair g (k2, pk2) <- atomically $ generateKeyPair g kems <- kemParams - pure (pk1, pk2, snd <$> kems, E2ERatchetParams v k1 k2 (fst <$> kems)) + pure ((pk1, pk2, snd <$> kems), E2ERatchetParams v k1 k2 (fst <$> kems)) where kemParams :: IO (Maybe (RKEMParams s, PrivRKEMParams s)) kemParams = case useKEM_ of @@ -436,7 +443,7 @@ generateE2EParams g v useKEM_ = do _ -> pure Nothing -- used by party initiating connection, Bob in double-ratchet spec -generateRcvE2EParams :: (AlgorithmI a, DhAlgorithm a) => TVar ChaChaDRG -> VersionE2E -> PQSupport -> IO (PrivateKey a, PrivateKey a, Maybe (PrivRKEMParams 'RKSProposed), E2ERatchetParams 'RKSProposed a) +generateRcvE2EParams :: (AlgorithmI a, DhAlgorithm a) => TVar ChaChaDRG -> VersionE2E -> PQSupport -> IO (RcvE2EPrivRatchetParams a, RcvE2ERatchetParams a) generateRcvE2EParams g v = generateE2EParams g v . proposeKEM_ where proposeKEM_ :: PQSupport -> Maybe (UseKEM 'RKSProposed) @@ -445,14 +452,14 @@ generateRcvE2EParams g v = generateE2EParams g v . proposeKEM_ PQSupportOff -> Nothing -- used by party accepting connection, Alice in double-ratchet spec -generateSndE2EParams :: forall a. (AlgorithmI a, DhAlgorithm a) => TVar ChaChaDRG -> VersionE2E -> Maybe AUseKEM -> IO (PrivateKey a, PrivateKey a, Maybe APrivRKEMParams, AE2ERatchetParams a) +generateSndE2EParams :: forall a. (AlgorithmI a, DhAlgorithm a) => TVar ChaChaDRG -> VersionE2E -> Maybe AUseKEM -> IO (AE2EPrivRatchetParams a, AE2ERatchetParams a) generateSndE2EParams g v = \case Nothing -> do - (pk1, pk2, _, e2eParams) <- generateE2EParams g v Nothing - pure (pk1, pk2, Nothing, AE2ERatchetParams SRKSProposed e2eParams) + ((pk1, pk2, _), e2eParams) <- generateE2EParams g v Nothing + pure ((pk1, pk2, Nothing), AE2ERatchetParams SRKSProposed e2eParams) Just (AUseKEM s useKEM) -> do - (pk1, pk2, pKem, e2eParams) <- generateE2EParams g v (Just useKEM) - pure (pk1, pk2, APRKP s <$> pKem, AE2ERatchetParams s e2eParams) + ((pk1, pk2, pKem), e2eParams) <- generateE2EParams g v (Just useKEM) + pure ((pk1, pk2, APRKP s <$> pKem), AE2ERatchetParams s e2eParams) data RatchetInitParams = RatchetInitParams { assocData :: Str, @@ -464,9 +471,9 @@ data RatchetInitParams = RatchetInitParams deriving (Show) -- this is used by the peer joining the connection -pqX3dhSnd :: DhAlgorithm a => PrivateKey a -> PrivateKey a -> Maybe APrivRKEMParams -> E2ERatchetParams 'RKSProposed a -> Either CryptoError (RatchetInitParams, Maybe KEMKeyPair) +pqX3dhSnd :: DhAlgorithm a => AE2EPrivRatchetParams a -> E2ERatchetParams 'RKSProposed a -> Either CryptoError (RatchetInitParams, Maybe KEMKeyPair) -- 3. replied 2. received -pqX3dhSnd spk1 spk2 spKem_ (E2ERatchetParams v rk1 rk2 rKem_) = do +pqX3dhSnd (spk1, spk2, spKem_) (E2ERatchetParams v rk1 rk2 rKem_) = do (ks_, kem_) <- sndPq let initParams = pqX3dh (publicKey spk1, rk1) (dh' rk1 spk2) (dh' rk2 spk1) (dh' rk2 spk2) kem_ pure (initParams, ks_) @@ -480,9 +487,9 @@ pqX3dhSnd spk1 spk2 spKem_ (E2ERatchetParams v rk1 rk2 rKem_) = do _ -> Right (Nothing, Nothing) -- this is used by the peer that created new connection, after receiving the reply -pqX3dhRcv :: forall s a. (RatchetKEMStateI s, DhAlgorithm a) => PrivateKey a -> PrivateKey a -> Maybe (PrivRKEMParams 'RKSProposed) -> E2ERatchetParams s a -> ExceptT CryptoError IO (RatchetInitParams, Maybe KEMKeyPair) +pqX3dhRcv :: forall s a. (RatchetKEMStateI s, DhAlgorithm a) => RcvE2EPrivRatchetParams a -> E2ERatchetParams s a -> ExceptT CryptoError IO (RatchetInitParams, Maybe KEMKeyPair) -- 1. sent 4. received in reply -pqX3dhRcv rpk1 rpk2 rpKem_ (E2ERatchetParams v sk1 sk2 sKem_) = do +pqX3dhRcv (rpk1, rpk2, rpKem_) (E2ERatchetParams v sk1 sk2 sKem_) = do kem_ <- rcvPq let initParams = pqX3dh (sk1, publicKey rpk1) (dh' sk2 rpk1) (dh' sk1 rpk2) (dh' sk2 rpk2) (snd <$> kem_) pure (initParams, fst <$> kem_) diff --git a/src/Simplex/Messaging/Notifications/Transport.hs b/src/Simplex/Messaging/Notifications/Transport.hs index 3df1e53ea..bf5683789 100644 --- a/src/Simplex/Messaging/Notifications/Transport.hs +++ b/src/Simplex/Messaging/Notifications/Transport.hs @@ -186,5 +186,6 @@ ntfTHandle c = THandle {connection = c, params} implySessId = False, encryptBlock = Nothing, batch = False, - serviceAuth = False + serviceAuth = False, + serverInfo = Nothing } diff --git a/src/Simplex/Messaging/Protocol.hs b/src/Simplex/Messaging/Protocol.hs index 8ef5bb04f..8730597f3 100644 --- a/src/Simplex/Messaging/Protocol.hs +++ b/src/Simplex/Messaging/Protocol.hs @@ -1546,11 +1546,14 @@ queueIdHash = IdsHash . C.md5Hash . unEntityId {-# INLINE queueIdHash #-} addServiceSubs :: (Int64, IdsHash) -> (Int64, IdsHash) -> (Int64, IdsHash) -addServiceSubs (n', idsHash') (n, idsHash) = (n + n', idsHash <> idsHash') +addServiceSubs (n', idsHash') (n, idsHash) = + let !n'' = n + n' + !h = idsHash <> idsHash' + in (n'', h) subtractServiceSubs :: (Int64, IdsHash) -> (Int64, IdsHash) -> (Int64, IdsHash) subtractServiceSubs (n', idsHash') (n, idsHash) - | n > n' = (n - n', idsHash <> idsHash') -- concat is a reversible xor: (x `xor` y) `xor` y == x + | n > n' = let !n'' = n - n'; !h = idsHash <> idsHash' in (n'', h) -- concat is a reversible xor: (x `xor` y) `xor` y == x | otherwise = (0, mempty) data ProtocolErrorType = PECmdSyntax | PECmdUnknown | PESession | PEBlock diff --git a/src/Simplex/Messaging/Server.hs b/src/Simplex/Messaging/Server.hs index 36ac471f9..b82addbbe 100644 --- a/src/Simplex/Messaging/Server.hs +++ b/src/Simplex/Messaging/Server.hs @@ -56,6 +56,7 @@ import Control.Monad.Trans.Except import Control.Monad.STM (retry) import Crypto.Random (ChaChaDRG) import Data.Bifunctor (first, second) +import qualified Data.Aeson as J import Data.ByteString.Base64 (encode) import qualified Data.ByteString.Builder as BLD import Data.ByteString.Char8 (ByteString) @@ -739,9 +740,10 @@ smpServer started cfg@ServerConfig {transports, transportConfig = tCfg, startOpt idSize <- asks $ queueIdBytes . config kh <- asks serverIdentity ks <- atomically . C.generateKeyPair =<< asks random - ServerConfig {smpServerVRange, smpHandshakeTimeout} <- asks config + ServerConfig {smpServerVRange, smpHandshakeTimeout, information} <- asks config + let serverInfo = LB.toStrict . J.encode <$> information labelMyThread $ "smp handshake for " <> transportName tp - liftIO (timeout smpHandshakeTimeout . runExceptT $ smpServerHandshake srvCert srvSignKey h ks kh smpServerVRange $ getClientService ms g idSize) >>= \case + liftIO (timeout smpHandshakeTimeout . runExceptT $ smpServerHandshake srvCert srvSignKey h ks kh smpServerVRange serverInfo $ getClientService ms g idSize) >>= \case Just (Right th) -> runClientTransport th _ -> pure () diff --git a/src/Simplex/Messaging/Server/Information.hs b/src/Simplex/Messaging/Server/Information.hs index d9b3891f7..2f1f046e7 100644 --- a/src/Simplex/Messaging/Server/Information.hs +++ b/src/Simplex/Messaging/Server/Information.hs @@ -26,7 +26,6 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Int (Int64) import Data.Maybe (isJust) import Data.Text (Text) -import Simplex.Messaging.Agent.Protocol (ConnectionLink, ConnectionMode (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON) @@ -112,7 +111,7 @@ data Entity = Entity {name :: Text, country :: Maybe Text} deriving (Show) data ServerContactAddress = ServerContactAddress - { simplex :: Maybe (ConnectionLink 'CMContact), + { simplex :: Maybe Text, email :: Maybe Text, -- it is recommended that it matches DNS email address, if either is present pgp :: Maybe PGPKey } diff --git a/src/Simplex/Messaging/Server/Main.hs b/src/Simplex/Messaging/Server/Main.hs index 098c18517..6b200611a 100644 --- a/src/Simplex/Messaging/Server/Main.hs +++ b/src/Simplex/Messaging/Server/Main.hs @@ -59,7 +59,7 @@ import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) import qualified Data.Text.IO as T import Options.Applicative -import Simplex.Messaging.Agent.Protocol (ConnectionLink (..), connReqUriP') +import Simplex.Messaging.Agent.Protocol (ConnectionLink (..), ConnectionMode (..), connReqUriP') import Simplex.Messaging.Agent.Store.Postgres.Options (DBOpts (..)) import Simplex.Messaging.Agent.Store.Shared (MigrationConfirmation (..)) import Simplex.Messaging.Client (HostMode (..), NetworkConfig (..), ProtocolClientConfig (..), SMPWebPortServers (..), SocksMode (..), defaultNetworkConfig, textToHostMode) @@ -85,7 +85,7 @@ import Simplex.Messaging.Transport (supportedProxyClientSMPRelayVRange, alpnSupp import Simplex.Messaging.Transport.Client (TransportHost (..), defaultSocksProxy) import Simplex.Messaging.Transport.HTTP2 (httpALPN) import Simplex.Messaging.Transport.Server (ServerCredentials (..), mkTransportServerConfig) -import Simplex.Messaging.Util (eitherToMaybe, ifM) +import Simplex.Messaging.Util (eitherToMaybe, ifM, safeDecodeUtf8) import System.Directory (createDirectoryIfMissing, doesDirectoryExist, doesFileExist) import System.Exit (exitFailure) import System.FilePath (combine) @@ -786,12 +786,13 @@ serverPublicInfo ini = serverInfo <$!> infoValue "source_code" <$!> infoValue nameField countryValue field = (either error id . validCountryValue (T.unpack field) . T.unpack) <$!> infoValue field iniContacts simplexField emailField pgpKeyUriField pgpKeyFingerprintField = - let simplex = either error id . parseAll linkP . encodeUtf8 <$!> eitherToMaybe (lookupValue "INFORMATION" simplexField ini) + let addr :: Maybe (ConnectionLink 'CMContact) = either error id . parseAll linkP . encodeUtf8 <$!> eitherToMaybe (lookupValue "INFORMATION" simplexField ini) + simplex = safeDecodeUtf8 . strEncode <$> addr linkP = CLFull <$> connReqUriP' Nothing <|> CLShort <$> strP email = infoValue emailField pkURI_ = infoValue pgpKeyUriField pkFingerprint_ = infoValue pgpKeyFingerprintField - in case (simplex, email, pkURI_, pkFingerprint_) of + in case (addr, email, pkURI_, pkFingerprint_) of (Nothing, Nothing, Nothing, _) -> Nothing (Nothing, Nothing, _, Nothing) -> Nothing (_, _, pkURI, pkFingerprint) -> Just ServerContactAddress {simplex, email, pgp = PGPKey <$> pkURI <*> pkFingerprint} diff --git a/src/Simplex/Messaging/Server/Web.hs b/src/Simplex/Messaging/Server/Web.hs index 7044a7e39..09e3180ed 100644 --- a/src/Simplex/Messaging/Server/Web.hs +++ b/src/Simplex/Messaging/Server/Web.hs @@ -270,14 +270,14 @@ serverInfoSubsts simplexmqSource information = ] admin ServerContactAddress {simplex, email, pgp} = [ ("admin", Just ""), - ("adminSimplex", strEncode <$> simplex), + ("adminSimplex", encodeUtf8 <$> simplex), ("adminEmail", encodeUtf8 <$> email), ("adminPGP", encodeUtf8 . pkURI <$> pgp), ("adminPGPFingerprint", encodeUtf8 . pkFingerprint <$> pgp) ] complaints ServerContactAddress {simplex, email, pgp} = [ ("complaints", Just ""), - ("complaintsSimplex", strEncode <$> simplex), + ("complaintsSimplex", encodeUtf8 <$> simplex), ("complaintsEmail", encodeUtf8 <$> email), ("complaintsPGP", encodeUtf8 . pkURI <$> pgp), ("complaintsPGPFingerprint", encodeUtf8 . pkFingerprint <$> pgp) diff --git a/src/Simplex/Messaging/Transport.hs b/src/Simplex/Messaging/Transport.hs index 51a2955eb..3c6e86509 100644 --- a/src/Simplex/Messaging/Transport.hs +++ b/src/Simplex/Messaging/Transport.hs @@ -58,6 +58,7 @@ module Simplex.Messaging.Transport clientNoticesSMPVersion, rcvServiceSMPVersion, namesSMPVersion, + serverInfoSMPVersion, simplexMQVersion, smpBlockSize, TransportConfig (..), @@ -114,6 +115,7 @@ import Control.Monad import Control.Monad.Except import Control.Monad.IO.Class import Control.Monad.Trans.Except (throwE) +import qualified Data.Aeson as J import qualified Data.Aeson.TH as J import Data.Attoparsec.ByteString.Char8 (Parser) import qualified Data.Attoparsec.ByteString.Char8 as A @@ -140,9 +142,10 @@ import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, parseRead1, sumTypeJSON) +import Simplex.Messaging.Server.Information import Simplex.Messaging.Transport.Buffer import Simplex.Messaging.Transport.Shared -import Simplex.Messaging.Util (bshow, catchAll, catchAll_, liftEitherWith) +import Simplex.Messaging.Util (bshow, catchAll, catchAll_, liftEitherWith, (<$$>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal import System.IO.Error (isEOFError) @@ -173,6 +176,7 @@ smpBlockSize = 16384 -- 17 - create notification credentials with NEW (7/12/2025) -- 18 - support client notices (10/10/2025) -- 19 - service subscriptions to messages (10/20/2025) +-- 20 - server public information in handshake (7/5/2026) data SMPVersion @@ -227,6 +231,9 @@ rcvServiceSMPVersion = VersionSMP 19 namesSMPVersion :: VersionSMP namesSMPVersion = VersionSMP 20 +serverInfoSMPVersion :: VersionSMP +serverInfoSMPVersion = VersionSMP 20 + minClientSMPRelayVersion :: VersionSMP minClientSMPRelayVersion = VersionSMP 6 @@ -496,7 +503,9 @@ data THandleParams v p = THandleParams -- based on protocol version batch :: Bool, -- | include service signature (or '0' if it is absent), based on protocol version - serviceAuth :: Bool + serviceAuth :: Bool, + -- | JSON-encoded ServerPublicInfo from handshake, present when server version >= serverInfoSMPVersion + serverInfo :: Maybe (Either String ServerPublicInfo) } data THandleAuth (p :: TransportPeer) where @@ -548,7 +557,9 @@ data SMPServerHandshake = SMPServerHandshake sessionId :: SessionId, -- pub key to agree shared secrets for command authorization and entity ID encryption. -- todo C.PublicKeyX25519 - authPubKey :: Maybe CertChainPubKey + authPubKey :: Maybe CertChainPubKey, + -- | optional server public information (JSON-encoded ServerPublicInfo), sent when version >= serverInfoSMPVersion + serverInfoBytes :: Maybe ByteString } -- This is the third handshake message that SMP server sends to services @@ -638,16 +649,21 @@ ifHasProxy v a b = if v >= proxyServerHandshakeSMPVersion then a else b ifHasService :: VersionSMP -> a -> a -> a ifHasService v a b = if v >= serviceCertsSMPVersion then a else b +ifHasServerInfo :: VersionSMP -> a -> a -> a +ifHasServerInfo v a b = if v >= serverInfoSMPVersion then a else b + instance Encoding SMPServerHandshake where - smpEncode SMPServerHandshake {smpVersionRange, sessionId, authPubKey} = - smpEncode (smpVersionRange, sessionId) <> auth + smpEncode SMPServerHandshake {smpVersionRange, sessionId, authPubKey, serverInfoBytes} = + smpEncode (smpVersionRange, sessionId) <> auth <> info where auth = encodeAuthEncryptCmds (maxVersion smpVersionRange) authPubKey + info = ifHasServerInfo (maxVersion smpVersionRange) (smpEncode (Large <$> serverInfoBytes)) "" smpP = do (smpVersionRange, sessionId) <- smpP -- TODO drop SMP v6: remove special parser and make key non-optional authPubKey <- authEncryptCmdsP (maxVersion smpVersionRange) smpP - pure SMPServerHandshake {smpVersionRange, sessionId, authPubKey} + serverInfoBytes <- ifHasServerInfo (maxVersion smpVersionRange) (unLarge <$$> smpP) (pure Nothing) + pure SMPServerHandshake {smpVersionRange, sessionId, authPubKey, serverInfoBytes} -- newtype for CertificateChain and a session key signed with this certificate data CertChainPubKey = CertChainPubKey @@ -763,12 +779,13 @@ smpServerHandshake :: C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> + Maybe ByteString -> (SMPServiceRole -> X.CertificateChain -> XV.Fingerprint -> ExceptT TransportError IO ServiceId) -> ExceptT TransportError IO (THandleSMP c 'TServer) -smpServerHandshake srvCert srvSignKey c (k, pk) kh smpVRange getService = do +smpServerHandshake srvCert srvSignKey c (k, pk) kh smpVRange serverInfoBytes getService = do let sk = C.signX509 srvSignKey $ C.publicToX509 k smpVersionRange = maybe legacyServerSMPRelayVRange (const smpVRange) $ getSessionALPN c - sendHandshake th $ SMPServerHandshake {sessionId, smpVersionRange, authPubKey = Just (CertChainPubKey srvCert sk)} + sendHandshake th $ SMPServerHandshake {sessionId, smpVersionRange, authPubKey = Just (CertChainPubKey srvCert sk), serverInfoBytes} SMPClientHandshake {smpVersion = v, keyHash, authPubKey = k', proxyServer, clientService} <- getHandshake th when (keyHash /= kh) $ throwE $ TEHandshake IDENTITY case compatibleVRange' smpVersionRange v of @@ -801,7 +818,7 @@ smpServerHandshake srvCert srvSignKey c (k, pk) kh smpVRange getService = do -- See https://github.com/simplex-chat/simplexmq/blob/master/protocol/simplex-messaging.md#appendix-a smpClientHandshake :: forall c. Transport c => c 'TClient -> Maybe C.KeyPairX25519 -> C.KeyHash -> VersionRangeSMP -> Bool -> Maybe (ServiceCredentials, C.KeyPairEd25519) -> ExceptT TransportError IO (THandleSMP c 'TClient) smpClientHandshake c ks_ keyHash@(C.KeyHash kh) vRange proxyServer serviceKeys_ = do - SMPServerHandshake {sessionId = sessId, smpVersionRange, authPubKey} <- getHandshake th + SMPServerHandshake {sessionId = sessId, smpVersionRange, authPubKey, serverInfoBytes} <- getHandshake th when (sessionId /= sessId) $ throwE TEBadSession -- Below logic downgrades version range in case the "client" is SMP proxy server and it is -- connected to the destination server of the version 11 or older. @@ -838,7 +855,7 @@ smpClientHandshake c ks_ keyHash@(C.KeyHash kh) vRange proxyServer serviceKeys_ hs = SMPClientHandshake {smpVersion = v, keyHash, authPubKey = fst <$> ks_, proxyServer, clientService} sendHandshake th hs service <- mapM getClientService serviceKeys - liftIO $ smpTHandleClient th v vr (snd <$> ks_) ck_ proxyServer service + liftIO $ smpTHandleClient th v vr (snd <$> ks_) ck_ proxyServer service serverInfoBytes Nothing -> throwE TEVersion where th@THandle {params = THandleParams {sessionId}} = smpTHandle c @@ -856,14 +873,14 @@ smpTHandleServer :: forall c. THandleSMP c 'TServer -> VersionSMP -> VersionRang smpTHandleServer th v vr pk k_ proxyServer peerClientService = do let thAuth = Just THAuthServer {serverPrivKey = pk, peerClientService, sessSecret' = (`C.dh'` pk) <$!> k_} be <- blockEncryption th v proxyServer thAuth - pure $ smpTHandle_ th v vr thAuth $ uncurry TSbChainKeys <$> be + pure $ smpTHandle_ th v vr thAuth (uncurry TSbChainKeys <$> be) Nothing -smpTHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, CertChainPubKey) -> Bool -> Maybe THClientService -> IO (THandleSMP c 'TClient) -smpTHandleClient th v vr pk_ ck_ proxyServer clientService = do +smpTHandleClient :: forall c. THandleSMP c 'TClient -> VersionSMP -> VersionRangeSMP -> Maybe C.PrivateKeyX25519 -> Maybe (C.PublicKeyX25519, CertChainPubKey) -> Bool -> Maybe THClientService -> Maybe ByteString -> IO (THandleSMP c 'TClient) +smpTHandleClient th v vr pk_ ck_ proxyServer clientService serverInfoBytes = do let thAuth = clientTHParams <$!> ck_ be <- blockEncryption th v proxyServer thAuth -- swap is needed to use client's sndKey as server's rcvKey and vice versa - pure $ smpTHandle_ th v vr thAuth $ uncurry TSbChainKeys . swap <$> be + pure $ smpTHandle_ th v vr thAuth (uncurry TSbChainKeys . swap <$> be) serverInfoBytes where clientTHParams (k, ck) = THAuthClient @@ -883,8 +900,8 @@ blockEncryption THandle {params = THandleParams {sessionId}} v proxyServer = \ca be :: Maybe C.DhSecretX25519 -> IO (Maybe (TVar C.SbChainKey, TVar C.SbChainKey)) be = mapM $ \(C.DhSecretX25519 secret) -> bimapM newTVarIO newTVarIO $ C.sbcInit sessionId secret -smpTHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> Maybe TSbChainKeys -> THandleSMP c p -smpTHandle_ th@THandle {params} v vr thAuth encryptBlock = +smpTHandle_ :: forall c p. THandleSMP c p -> VersionSMP -> VersionRangeSMP -> Maybe (THandleAuth p) -> Maybe TSbChainKeys -> Maybe ByteString -> THandleSMP c p +smpTHandle_ th@THandle {params} v vr thAuth encryptBlock serverInfoBytes = -- TODO drop SMP v6: make thAuth non-optional -- * Note: update version-based parameters in smpTHParamsSetVersion as well. let params' = @@ -894,7 +911,8 @@ smpTHandle_ th@THandle {params} v vr thAuth encryptBlock = thAuth, implySessId = v >= authCmdsSMPVersion, encryptBlock, - serviceAuth = v >= serviceCertsSMPVersion -- optional service signature will be encoded for all commands and responses + serviceAuth = v >= serviceCertsSMPVersion, -- optional service signature will be encoded for all commands and responses + serverInfo = J.eitherDecodeStrict' <$> serverInfoBytes } in (th :: THandleSMP c p) {params = params'} @@ -933,7 +951,8 @@ smpTHandle c = THandle {connection = c, params} implySessId = False, encryptBlock = Nothing, batch = True, - serviceAuth = False + serviceAuth = False, + serverInfo = Nothing } $(J.deriveJSON (sumTypeJSON id) ''HandshakeError) diff --git a/tests/AgentTests/DoubleRatchetTests.hs b/tests/AgentTests/DoubleRatchetTests.hs index eef5be27f..5c7ce80d5 100644 --- a/tests/AgentTests/DoubleRatchetTests.hs +++ b/tests/AgentTests/DoubleRatchetTests.hs @@ -381,19 +381,19 @@ testX3dh :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testX3dh _ = do g <- C.newRandom let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion - (pkBob1, pkBob2, Nothing, AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v Nothing - (pkAlice1, pkAlice2, Nothing, e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOff - let paramsBob = pqX3dhSnd pkBob1 pkBob2 Nothing e2eAlice - paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 Nothing e2eBob + (pksBob@(_, _, Nothing), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v Nothing + (pksAlice@(_, _, Nothing), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOff + let paramsBob = pqX3dhSnd pksBob e2eAlice + paramsAlice <- runExceptT $ pqX3dhRcv pksAlice e2eBob paramsAlice `shouldBe` paramsBob testX3dhV1 :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testX3dhV1 _ = do g <- C.newRandom - (pkBob1, pkBob2, Nothing, AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g (VersionE2E 1) Nothing - (pkAlice1, pkAlice2, Nothing, e2eAlice) <- liftIO $ generateRcvE2EParams @a g (VersionE2E 1) PQSupportOff - let paramsBob = pqX3dhSnd pkBob1 pkBob2 Nothing e2eAlice - paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 Nothing e2eBob + (pksBob@(_, _, Nothing), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g (VersionE2E 1) Nothing + (pksAlice@(_, _, Nothing), e2eAlice) <- liftIO $ generateRcvE2EParams @a g (VersionE2E 1) PQSupportOff + let paramsBob = pqX3dhSnd pksBob e2eAlice + paramsAlice <- runExceptT $ pqX3dhRcv pksAlice e2eBob paramsAlice `shouldBe` paramsBob testPqX3dhProposeInReply :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () @@ -401,11 +401,11 @@ testPqX3dhProposeInReply _ = do g <- C.newRandom let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (no KEM) - (pkAlice1, pkAlice2, Nothing, e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOff + (pksAlice@(_, _, Nothing), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOff -- propose KEM in reply - (pkBob1, pkBob2, pKemBob_@(Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v (Just $ AUseKEM SRKSProposed ProposeKEM) - Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemBob_ e2eAlice - Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 Nothing e2eBob + (pksBob@(_, _, Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v (Just $ AUseKEM SRKSProposed ProposeKEM) + Right paramsBob <- pure $ pqX3dhSnd pksBob e2eAlice + Right paramsAlice <- runExceptT $ pqX3dhRcv pksAlice e2eBob paramsAlice `compatibleRatchets` paramsBob testPqX3dhProposeAccept :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () @@ -413,12 +413,12 @@ testPqX3dhProposeAccept _ = do g <- C.newRandom let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (propose KEM) - (pkAlice1, pkAlice2, pKemAlice_@(Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOn + (pksAlice@(_, _, Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOn E2ERatchetParams _ _ _ (Just (RKParamsProposed aliceKem)) <- pure e2eAlice -- accept KEM - (pkBob1, pkBob2, pKemBob_@(Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v (Just $ AUseKEM SRKSAccepted $ AcceptKEM aliceKem) - Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemBob_ e2eAlice - Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 pKemAlice_ e2eBob + (pksBob@(_, _, Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v (Just $ AUseKEM SRKSAccepted $ AcceptKEM aliceKem) + Right paramsBob <- pure $ pqX3dhSnd pksBob e2eAlice + Right paramsAlice <- runExceptT $ pqX3dhRcv pksAlice e2eBob paramsAlice `compatibleRatchets` paramsBob testPqX3dhProposeReject :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () @@ -426,12 +426,12 @@ testPqX3dhProposeReject _ = do g <- C.newRandom let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (propose KEM) - (pkAlice1, pkAlice2, pKemAlice_@(Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOn + (pksAlice@(_, _, Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOn E2ERatchetParams _ _ _ (Just (RKParamsProposed _)) <- pure e2eAlice -- reject KEM - (pkBob1, pkBob2, Nothing, AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v Nothing - Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 Nothing e2eAlice - Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 pKemAlice_ e2eBob + (pksBob@(_, _, Nothing), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v Nothing + Right paramsBob <- pure $ pqX3dhSnd pksBob e2eAlice + Right paramsAlice <- runExceptT $ pqX3dhRcv pksAlice e2eBob paramsAlice `compatibleRatchets` paramsBob testPqX3dhAcceptWithoutProposalError :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () @@ -439,26 +439,26 @@ testPqX3dhAcceptWithoutProposalError _ = do g <- C.newRandom let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (no KEM) - (pkAlice1, pkAlice2, Nothing, e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOff + (pksAlice@(_, _, Nothing), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOff E2ERatchetParams _ _ _ Nothing <- pure e2eAlice -- incorrectly accept KEM -- we don't have key in proposal, so we just generate it (k, _) <- sntrup761Keypair g - (pkBob1, pkBob2, pKemBob_@(Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v (Just $ AUseKEM SRKSAccepted $ AcceptKEM k) - pqX3dhSnd pkBob1 pkBob2 pKemBob_ e2eAlice `shouldBe` Left C.CERatchetKEMState - runExceptT (pqX3dhRcv pkAlice1 pkAlice2 Nothing e2eBob) `shouldReturn` Left C.CERatchetKEMState + (pksBob@(_, _, Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v (Just $ AUseKEM SRKSAccepted $ AcceptKEM k) + pqX3dhSnd pksBob e2eAlice `shouldBe` Left C.CERatchetKEMState + runExceptT (pqX3dhRcv pksAlice e2eBob) `shouldReturn` Left C.CERatchetKEMState testPqX3dhProposeAgain :: forall a. (AlgorithmI a, DhAlgorithm a) => C.SAlgorithm a -> IO () testPqX3dhProposeAgain _ = do g <- C.newRandom let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (propose KEM) - (pkAlice1, pkAlice2, pKemAlice_@(Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOn + (pksAlice@(_, _, Just _), e2eAlice) <- liftIO $ generateRcvE2EParams @a g v PQSupportOn E2ERatchetParams _ _ _ (Just (RKParamsProposed _)) <- pure e2eAlice -- propose KEM again in reply - this is not an error - (pkBob1, pkBob2, pKemBob_@(Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v (Just $ AUseKEM SRKSProposed ProposeKEM) - Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemBob_ e2eAlice - Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 pKemAlice_ e2eBob + (pksBob@(_, _, Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams @a g v (Just $ AUseKEM SRKSProposed ProposeKEM) + Right paramsBob <- pure $ pqX3dhSnd pksBob e2eAlice + Right paramsAlice <- runExceptT $ pqX3dhRcv pksAlice e2eBob paramsAlice `compatibleRatchets` paramsBob compatibleRatchets :: (RatchetInitParams, x) -> (RatchetInitParams, x) -> Expectation @@ -515,10 +515,10 @@ initRatchets :: (AlgorithmI a, DhAlgorithm a) => IO (Ratchet a, Ratchet a, Encry initRatchets = do g <- C.newRandom let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion - (pkBob1, pkBob2, _pKemParams@Nothing, AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams g v Nothing - (pkAlice1, pkAlice2, _pKem@Nothing, e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOff - Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 Nothing e2eAlice - Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 Nothing e2eBob + (pksBob@(_, _, Nothing), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams g v Nothing + (pksAlice@(_, pkAlice2, Nothing), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOff + Right paramsBob <- pure $ pqX3dhSnd pksBob e2eAlice + Right paramsAlice <- runExceptT $ pqX3dhRcv pksAlice e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g let vs = testRatchetVersions bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob @@ -530,12 +530,12 @@ initRatchetsKEMProposed = do g <- C.newRandom let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (no KEM) - (pkAlice1, pkAlice2, Nothing, e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOff + (pksAlice@(_, pkAlice2, Nothing), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOff -- propose KEM in reply let useKem = AUseKEM SRKSProposed ProposeKEM - (pkBob1, pkBob2, pKemParams_@(Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams g v (Just useKem) - Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemParams_ e2eAlice - Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 Nothing e2eBob + (pksBob@(_, _, Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams g v (Just useKem) + Right paramsBob <- pure $ pqX3dhSnd pksBob e2eAlice + Right paramsAlice <- runExceptT $ pqX3dhRcv pksAlice e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g let vs = testRatchetVersions bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob @@ -547,13 +547,13 @@ initRatchetsKEMAccepted = do g <- C.newRandom let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (propose) - (pkAlice1, pkAlice2, pKem_@(Just _), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOn + (pksAlice@(_, pkAlice2, Just _), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOn E2ERatchetParams _ _ _ (Just (RKParamsProposed aliceKem)) <- pure e2eAlice -- accept let useKem = AUseKEM SRKSAccepted (AcceptKEM aliceKem) - (pkBob1, pkBob2, pKemParams_@(Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams g v (Just useKem) - Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemParams_ e2eAlice - Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 pKem_ e2eBob + (pksBob@(_, _, Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams g v (Just useKem) + Right paramsBob <- pure $ pqX3dhSnd pksBob e2eAlice + Right paramsAlice <- runExceptT $ pqX3dhRcv pksAlice e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g let vs = testRatchetVersions bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob @@ -565,12 +565,12 @@ initRatchetsKEMProposedAgain = do g <- C.newRandom let v = max pqRatchetE2EEncryptVersion currentE2EEncryptVersion -- initiate (propose KEM) - (pkAlice1, pkAlice2, pKem_@(Just _), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOn + (pksAlice@(_, pkAlice2, Just _), e2eAlice) <- liftIO $ generateRcvE2EParams g v PQSupportOn -- propose KEM again in reply let useKem = AUseKEM SRKSProposed ProposeKEM - (pkBob1, pkBob2, pKemParams_@(Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams g v (Just useKem) - Right paramsBob <- pure $ pqX3dhSnd pkBob1 pkBob2 pKemParams_ e2eAlice - Right paramsAlice <- runExceptT $ pqX3dhRcv pkAlice1 pkAlice2 pKem_ e2eBob + (pksBob@(_, _, Just _), AE2ERatchetParams _ e2eBob) <- liftIO $ generateSndE2EParams g v (Just useKem) + Right paramsBob <- pure $ pqX3dhSnd pksBob e2eAlice + Right paramsAlice <- runExceptT $ pqX3dhRcv pksAlice e2eBob (_, pkBob3) <- atomically $ C.generateKeyPair g let vs = testRatchetVersions bob = initSndRatchet vs (C.publicKey pkAlice2) pkBob3 paramsBob diff --git a/tests/AgentTests/EqInstances.hs b/tests/AgentTests/EqInstances.hs index b01174343..b701482fe 100644 --- a/tests/AgentTests/EqInstances.hs +++ b/tests/AgentTests/EqInstances.hs @@ -8,6 +8,7 @@ import Data.Type.Equality import Simplex.Messaging.Agent.Protocol (ShortLinkCreds (..)) import Simplex.Messaging.Agent.Store import Simplex.Messaging.Client (ProxiedRelay (..)) +import Simplex.Messaging.Server.Information instance (Eq rq, Eq sq) => Eq (SomeConn' rq sq) where SomeConn d c == SomeConn d' c' = case testEquality d d' of @@ -31,3 +32,15 @@ deriving instance Eq ShortLinkCreds deriving instance Show ProxiedRelay deriving instance Eq ProxiedRelay + +deriving instance Eq Entity + +deriving instance Eq HostingType + +deriving instance Eq PGPKey + +deriving instance Eq ServerConditions + +deriving instance Eq ServerContactAddress + +deriving instance Eq ServerPublicInfo diff --git a/tests/AgentTests/FunctionalAPITests.hs b/tests/AgentTests/FunctionalAPITests.hs index fba0eac4a..73897f789 100644 --- a/tests/AgentTests/FunctionalAPITests.hs +++ b/tests/AgentTests/FunctionalAPITests.hs @@ -106,6 +106,7 @@ import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Protocol.Types import Simplex.Messaging.Server.Env.STM (AStoreType (..), ServerConfig (..), ServerStoreCfg (..), StorePaths (..)) import Simplex.Messaging.Server.Expiration +import Simplex.Messaging.Server.Information (ServerPublicInfo) import Simplex.Messaging.Server.MsgStore.Types (SMSType (..), SQSType (..)) import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.Server.StoreLog (StoreLogRecord (..)) @@ -534,17 +535,17 @@ functionalAPITests ps = do it "auth both " $ testBasicAuth ps True (Nothing, v) (Just "abcd", v) (Just "abcd", v) sqSecured baseId `shouldReturn` 2 it "auth, disabled" $ testBasicAuth ps False (Nothing, v) (Just "abcd", v) (Just "abcd", v) sqSecured baseId `shouldReturn` 0 describe "SMP server test via agent API" $ do - it "should pass without basic auth" $ testSMPServerConnectionTest ps Nothing (noAuthSrv testSMPServer2) `shouldReturn` Nothing + it "should pass without basic auth" $ testSMPServerConnectionTest ps Nothing (noAuthSrv testSMPServer2) `shouldReturn` Right Nothing let srv1 = testSMPServer2 {keyHash = "1234"} it "should fail with incorrect fingerprint" $ do - testSMPServerConnectionTest ps Nothing (noAuthSrv srv1) `shouldReturn` Just (ProtocolTestFailure TSConnect $ BROKER (B.unpack $ strEncode srv1) $ NETWORK NEUnknownCAError) + testSMPServerConnectionTest ps Nothing (noAuthSrv srv1) `shouldReturn` Left (ProtocolTestFailure TSConnect $ BROKER (B.unpack $ strEncode srv1) $ NETWORK NEUnknownCAError) describe "server with password" $ do let auth = Just "abcd" srv = ProtoServerWithAuth testSMPServer2 - authErr = Just (ProtocolTestFailure TSCreateQueue $ SMP (B.unpack $ strEncode testSMPServer2) AUTH) - it "should pass with correct password" $ testSMPServerConnectionTest ps auth (srv auth) `shouldReturn` Nothing - it "should fail without password" $ testSMPServerConnectionTest ps auth (srv Nothing) `shouldReturn` authErr - it "should fail with incorrect password" $ testSMPServerConnectionTest ps auth (srv $ Just "wrong") `shouldReturn` authErr + authErr = ProtocolTestFailure TSCreateQueue $ SMP (B.unpack $ strEncode testSMPServer2) AUTH + it "should pass with correct password" $ testSMPServerConnectionTest ps auth (srv auth) `shouldReturn` Right Nothing + it "should fail without password" $ testSMPServerConnectionTest ps auth (srv Nothing) `shouldReturn` Left authErr + it "should fail with incorrect password" $ testSMPServerConnectionTest ps auth (srv $ Just "wrong") `shouldReturn` Left authErr describe "getRatchetAdHash" $ it "should return the same data for both peers" $ withSmpServer ps testRatchetAdHash @@ -1425,7 +1426,8 @@ testInvitationShortLinkAsync viaProxy a b = do connReq' `shouldBe` connReq linkUserData connData' `shouldBe` userData runRight $ do - aId <- A.joinConnectionAsync b 1 "123" Nothing True connReq "bob's connInfo" PQSupportOn SMSubscribe + aId <- A.prepareConnectionToJoin b 1 True connReq PQSupportOn + A.joinConnectionAsync b "123" False aId True connReq "bob's connInfo" PQSupportOn SMSubscribe get b =##> \case ("123", c, JOINED sndSecure) -> c == aId && sndSecure; _ -> False ("", _, CONF confId _ "bob's connInfo") <- get a allowConnection a bId confId "alice's connInfo" @@ -1662,7 +1664,7 @@ testPrepareCreateConnectionLink ps = withSmpServer ps $ withAgentClients2 $ \a b linkEntId <- atomically $ C.randomBytes 32 g runRight $ do (ccLink@(CCLink connReq (Just shortLink)), preparedParams) <- - A.prepareConnectionLink a 1 rootKey linkEntId True Nothing + A.prepareConnectionLink a 1 rootKey linkEntId True Nothing Nothing liftIO $ strDecode (strEncode shortLink) `shouldBe` Right shortLink _ <- A.createConnectionForLink a NRMInteractive 1 True ccLink preparedParams userLinkData CR.IKPQOn SMSubscribe (FixedLinkData {linkConnReq = connReq', linkEntityId}, ContactLinkData _ userCtData') <- getConnShortLink b 1 shortLink @@ -2685,10 +2687,12 @@ receiveMsg c cId msgId msg = do testAsyncCommands :: SndQueueSecured -> AgentClient -> AgentClient -> AgentMsgId -> IO () testAsyncCommands sqSecured alice bob baseId = runRight_ $ do - bobId <- createConnectionAsync alice 1 "1" True SCMInvitation IKPQOn SMSubscribe + bobId <- prepareConnectionToCreate alice 1 True SCMInvitation PQSupportOn + createConnectionAsync alice "1" bobId True SCMInvitation IKPQOn SMSubscribe ("1", bobId', INV (ACR _ qInfo)) <- get alice liftIO $ bobId' `shouldBe` bobId - aliceId <- joinConnectionAsync bob 1 "2" Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe + aliceId <- prepareConnectionToJoin bob 1 True qInfo PQSupportOn + joinConnectionAsync bob "2" False aliceId True qInfo "bob's connInfo" PQSupportOn SMSubscribe ("2", aliceId', JOINED sqSecured') <- get bob liftIO $ do aliceId' `shouldBe` aliceId @@ -2779,8 +2783,8 @@ testGetConnShortLinkAsync ps = withAgentClients2 $ \alice bob -> liftIO $ qInfo' `shouldBe` qInfo liftIO $ userCtData' `shouldBe` userCtData -- join connection async using connId from getConnShortLinkAsync - aliceId <- joinConnectionAsync bob 1 "2" (Just newId) True qInfo' "bob's connInfo" PQSupportOn SMSubscribe - liftIO $ aliceId `shouldBe` newId + joinConnectionAsync bob "2" True newId True qInfo' "bob's connInfo" PQSupportOn SMSubscribe + let aliceId = newId ("2", aliceId', JOINED False) <- get bob liftIO $ aliceId' `shouldBe` aliceId -- complete connection @@ -2796,7 +2800,10 @@ testGetConnShortLinkAsync ps = withAgentClients2 $ \alice bob -> testAsyncCommandsRestore :: (ASrvTransport, AStoreType) -> IO () testAsyncCommandsRestore ps = do alice <- getSMPAgentClient' 1 agentCfg initAgentServers testDB - bobId <- runRight $ createConnectionAsync alice 1 "1" True SCMInvitation IKPQOn SMSubscribe + bobId <- runRight $ do + connId <- prepareConnectionToCreate alice 1 True SCMInvitation PQSupportOn + createConnectionAsync alice "1" connId True SCMInvitation IKPQOn SMSubscribe + pure connId liftIO $ noMessages alice "alice doesn't receive INV because server is down" disposeAgentClient alice withAgent 2 agentCfg initAgentServers testDB $ \alice' -> @@ -2812,7 +2819,8 @@ testAcceptContactAsync sqSecured alice bob baseId = (aliceId, sqSecuredJoin) <- joinConnection bob 1 True qInfo "bob's connInfo" SMSubscribe liftIO $ sqSecuredJoin `shouldBe` False -- joining via contact address connection ("", _, REQ invId _ "bob's connInfo") <- get alice - bobId <- acceptContactAsync alice 1 "1" True invId "alice's connInfo" PQSupportOn SMSubscribe + bobId <- prepareConnectionToAccept alice 1 True invId PQSupportOn + acceptContactAsync alice "1" bobId True invId "alice's connInfo" PQSupportOn SMSubscribe get alice =##> \case ("1", c, JOINED sqSecured') -> c == bobId && sqSecured' == sqSecured; _ -> False ("", _, CONF confId _ "alice's connInfo") <- get bob allowConnection bob aliceId confId "bob's connInfo" @@ -3083,10 +3091,12 @@ testJoinConnectionAsyncReplyErrorV8 ps@(t, ASType qsType _) = do withAgent 1 cfg' initAgentServers testDB $ \a -> withAgent 2 cfg' initAgentServersSrv2 testDB2 $ \b -> do (aId, bId) <- withSmpServerStoreLogOn ps testPort $ \_ -> runRight $ do - bId <- createConnectionAsync a 1 "1" True SCMInvitation IKPQOn SMSubscribe + bId <- prepareConnectionToCreate a 1 True SCMInvitation PQSupportOn + createConnectionAsync a "1" bId True SCMInvitation IKPQOn SMSubscribe ("1", bId', INV (ACR _ qInfo)) <- get a liftIO $ bId' `shouldBe` bId - aId <- joinConnectionAsync b 1 "2" Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe + aId <- prepareConnectionToJoin b 1 True qInfo PQSupportOn + joinConnectionAsync b "2" False aId True qInfo "bob's connInfo" PQSupportOn SMSubscribe liftIO $ threadDelay 500000 ConnectionStats {rcvQueuesInfo = [], sndQueuesInfo = [SndQueueInfo {}]} <- getConnectionServers b aId pure (aId, bId) @@ -3128,10 +3138,12 @@ testJoinConnectionAsyncReplyError ps@(t, ASType qsType _) = do withAgent 1 agentCfg initAgentServers testDB $ \a -> withAgent 2 agentCfg initAgentServersSrv2 testDB2 $ \b -> do (aId, bId) <- withSmpServerStoreLogOn ps testPort $ \_ -> runRight $ do - bId <- createConnectionAsync a 1 "1" True SCMInvitation IKPQOn SMSubscribe + bId <- prepareConnectionToCreate a 1 True SCMInvitation PQSupportOn + createConnectionAsync a "1" bId True SCMInvitation IKPQOn SMSubscribe ("1", bId', INV (ACR _ qInfo)) <- get a liftIO $ bId' `shouldBe` bId - aId <- joinConnectionAsync b 1 "2" Nothing True qInfo "bob's connInfo" PQSupportOn SMSubscribe + aId <- prepareConnectionToJoin b 1 True qInfo PQSupportOn + joinConnectionAsync b "2" False aId True qInfo "bob's connInfo" PQSupportOn SMSubscribe liftIO $ threadDelay 500000 ConnectionStats {rcvQueuesInfo = [], sndQueuesInfo = [SndQueueInfo {}]} <- getConnectionServers b aId pure (aId, bId) @@ -3624,7 +3636,7 @@ testCreateQueueAuth srvVersion clnt1 clnt2 sqSecured baseId = do sndAuthAlg = if srvVersion >= authCmdsSMPVersion && clntVersion >= authCmdsSMPVersion then C.AuthAlg C.SX25519 else C.AuthAlg C.SEd25519 in getSMPAgentClient' clientId agentCfg {smpCfg, sndAuthAlg} servers db -testSMPServerConnectionTest :: (ASrvTransport, AStoreType) -> Maybe BasicAuth -> SMPServerWithAuth -> IO (Maybe ProtocolTestFailure) +testSMPServerConnectionTest :: (ASrvTransport, AStoreType) -> Maybe BasicAuth -> SMPServerWithAuth -> IO (Either ProtocolTestFailure (Maybe (Either String ServerPublicInfo))) testSMPServerConnectionTest (t, msType) newQueueBasicAuth srv = withSmpServerConfigOn t cfg' testPort2 $ \_ -> do -- initially passed server is not running diff --git a/tests/AgentTests/NotificationTests.hs b/tests/AgentTests/NotificationTests.hs index 6a1c5cef9..826df66e6 100644 --- a/tests/AgentTests/NotificationTests.hs +++ b/tests/AgentTests/NotificationTests.hs @@ -82,6 +82,7 @@ import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (ErrorType (AUTH), NetworkError (..), MsgFlags (MsgFlags), NMsgMeta (..), NtfServer, ProtocolServer (..), SMPMsgMeta (..), SubscriptionMode (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Server.Env.STM (AStoreType (..), ServerConfig (..)) +import Simplex.Messaging.Server.Information (ServerPublicInfo (..)) import Simplex.Messaging.Transport (ASrvTransport) import Simplex.Messaging.Transport.Server (TransportServerConfig (..)) import System.Process (callCommand) @@ -134,10 +135,10 @@ notificationTests ps@(t, _) = do withAPNSMockServer $ \apns -> testNtfTokenReRegisterInvalidOnCheck t apns describe "notification server tests" $ do - it "should pass" $ testRunNTFServerTests t testNtfServer `shouldReturn` Nothing + it "should pass" $ testRunNTFServerTests t testNtfServer `shouldReturn` Right Nothing let srv1 = testNtfServer {keyHash = "1234"} it "should fail with incorrect fingerprint" $ do - testRunNTFServerTests t srv1 `shouldReturn` Just (ProtocolTestFailure TSConnect $ BROKER (B.unpack $ strEncode srv1) $ NETWORK NEUnknownCAError) + testRunNTFServerTests t srv1 `shouldReturn` Left (ProtocolTestFailure TSConnect $ BROKER (B.unpack $ strEncode srv1) $ NETWORK NEUnknownCAError) describe "Managing notification subscriptions" $ do describe "should create notification subscription for existing connection" $ testNtfMatrix ps testNotificationSubscriptionExistingConnection @@ -536,11 +537,11 @@ testNtfTokenReRegisterInvalidOnCheck t apns = do NTActive <- checkNtfToken a tkn1 pure () -testRunNTFServerTests :: ASrvTransport -> NtfServer -> IO (Maybe ProtocolTestFailure) +testRunNTFServerTests :: ASrvTransport -> NtfServer -> IO (Either ProtocolTestFailure (Maybe (Either String ServerPublicInfo))) testRunNTFServerTests t srv = withNtfServer t $ withAgent 1 agentCfg initAgentServers testDB $ \a -> - testProtocolServer a NRMInteractive 1 $ ProtoServerWithAuth srv Nothing + testProtocolServer a NRMInteractive 1 (ProtoServerWithAuth srv Nothing) testNotificationSubscriptionExistingConnection :: APNSMockServer -> AgentMsgId -> AgentClient -> AgentClient -> IO () testNotificationSubscriptionExistingConnection apns baseId alice@AgentClient {agentEnv = Env {config = aliceCfg, store}} bob = do diff --git a/tests/CoreTests/BatchingTests.hs b/tests/CoreTests/BatchingTests.hs index 8a285721b..8b8f62c16 100644 --- a/tests/CoreTests/BatchingTests.hs +++ b/tests/CoreTests/BatchingTests.hs @@ -421,7 +421,8 @@ testTHandleParams v sessionId = implySessId = v >= authCmdsSMPVersion, encryptBlock = Nothing, batch = True, - serviceAuth = v >= serviceCertsSMPVersion + serviceAuth = v >= serviceCertsSMPVersion, + serverInfo = Nothing } testTHandleAuth :: VersionSMP -> TVar ChaChaDRG -> C.APublicAuthKey -> IO (Maybe (THandleAuth 'TClient)) diff --git a/tests/XFTPAgent.hs b/tests/XFTPAgent.hs index af1d2f5c2..4976c9403 100644 --- a/tests/XFTPAgent.hs +++ b/tests/XFTPAgent.hs @@ -41,6 +41,7 @@ import qualified Simplex.Messaging.Crypto.File as CF import Simplex.Messaging.Encoding.String (StrEncoding (..)) import Simplex.Messaging.Protocol (BasicAuth, NetworkError (..), ProtoServerWithAuth (..), ProtocolServer (..), XFTPServerWithAuth) import Simplex.Messaging.Server.Expiration (ExpirationConfig (..)) +import Simplex.Messaging.Server.Information (ServerPublicInfo) import Simplex.Messaging.Util (tshow) import System.Directory (doesDirectoryExist, doesFileExist, getFileSize, listDirectory, removeFile) import System.FilePath (()) @@ -82,19 +83,19 @@ xftpAgentTests = it "if file is expired on server, should report error and continue receiving next file" testXFTPAgentExpiredOnServer it "should request additional recipient IDs when number of recipients exceeds maximum per request" testXFTPAgentRequestAdditionalRecipientIDs describe "XFTP server test via agent API" $ do - it "should pass without basic auth" $ \_ -> testXFTPServerTest Nothing (noAuthSrv testXFTPServer2) `shouldReturn` Nothing + it "should pass without basic auth" $ \_ -> testXFTPServerTest Nothing (noAuthSrv testXFTPServer2) `shouldReturn` Right Nothing let srv1 = testXFTPServer2 {keyHash = "1234"} it "should fail with incorrect fingerprint" $ \_ -> do - testXFTPServerTest Nothing (noAuthSrv srv1) `shouldReturn` Just (ProtocolTestFailure TSConnect $ BROKER (B.unpack $ strEncode srv1) $ NETWORK NEUnknownCAError) + testXFTPServerTest Nothing (noAuthSrv srv1) `shouldReturn` Left (ProtocolTestFailure TSConnect $ BROKER (B.unpack $ strEncode srv1) $ NETWORK NEUnknownCAError) describe "server with password" $ do let auth = Just "abcd" srv = ProtoServerWithAuth testXFTPServer2 - authErr = Just (ProtocolTestFailure TSCreateFile $ XFTP (B.unpack $ strEncode testXFTPServer2) AUTH) - it "should pass with correct password" $ \_ -> testXFTPServerTest auth (srv auth) `shouldReturn` Nothing - it "should fail without password" $ \_ -> testXFTPServerTest auth (srv Nothing) `shouldReturn` authErr - it "should fail with incorrect password" $ \_ -> testXFTPServerTest auth (srv $ Just "wrong") `shouldReturn` authErr + authErr = ProtocolTestFailure TSCreateFile $ XFTP (B.unpack $ strEncode testXFTPServer2) AUTH + it "should pass with correct password" $ \_ -> testXFTPServerTest auth (srv auth) `shouldReturn` Right Nothing + it "should fail without password" $ \_ -> testXFTPServerTest auth (srv Nothing) `shouldReturn` Left authErr + it "should fail with incorrect password" $ \_ -> testXFTPServerTest auth (srv $ Just "wrong") `shouldReturn` Left authErr -testXFTPServerTest :: HasCallStack => Maybe BasicAuth -> XFTPServerWithAuth -> IO (Maybe ProtocolTestFailure) +testXFTPServerTest :: HasCallStack => Maybe BasicAuth -> XFTPServerWithAuth -> IO (Either ProtocolTestFailure (Maybe (Either String ServerPublicInfo))) testXFTPServerTest newFileBasicAuth srv = withXFTPServerCfg testXFTPServerConfig {newFileBasicAuth, xftpPort = xftpTestPort2} $ \_ -> -- initially passed server is not running @@ -685,4 +686,4 @@ testXFTPServerTest_ :: HasCallStack => XFTPServerWithAuth -> IO (Maybe ProtocolT testXFTPServerTest_ srv = -- initially passed server is not running withAgent 1 agentCfg initAgentServers testDB $ \a -> - testProtocolServer a NRMInteractive 1 srv + either Just (const Nothing) <$> testProtocolServer a NRMInteractive 1 srv