-- | This module implements Scuttlebutt's Secret Handshake. -- -- For more information kindly refer the to protocol guide -- https://ssbc.github.io/scuttlebutt-protocol-guide -- | TODO: Take care of possible import loop -- | TODO: Optimize handling of PublicKey (extractPublicKey) module Ssb.Peer.SecretHandshake where import Protolude hiding ( Identity ) import qualified Data.ByteString as BS import Data.Default import qualified Crypto.Hash.SHA256 as SHA256 import qualified Crypto.Saltine.Core.ScalarMult as ScalarMult import qualified Crypto.Saltine.Class as Nacl import qualified Crypto.Saltine.Core.Auth as Auth import qualified Crypto.Saltine.Core.Box as Box import qualified Crypto.Saltine.Core.SecretBox as SecretBox import qualified Crypto.Saltine.Core.Sign as Sign import Ssb.Network import Ssb.Identity import qualified Sodium -- | ChallengeLength is the length of a challenge message in bytes challengeLength :: Int challengeLength = 64 -- | ClientAuthLength is the length of a clientAuth message in bytes clientAuthLength :: Int clientAuthLength = 16 + 32 + 64 -- | ServerAcceptLength is the length of a serverAccept message in bytes serverAcceptLength :: Int serverAcceptLength = 16 + 64 -- | MACLength is the length of a MAC in bytes macLength :: Int macLength = 16 -- | NetworkIdentifier defines which of the possible networks is being used. -- Most traffic is on MainNet, and others may be used for testing purposes. type NetworkIdentifier = ByteString type SharedSecret = ScalarMult.GroupElement -- | SharedSecrets are the result of Scuttlebutt's handshake -- TODO: make shared secrets readable and showable data SharedSecrets = SharedSecrets { network :: NetworkIdentifier , secreta :: Maybe PublicKey , secretA :: Maybe PublicKey , secretb :: Maybe PublicKey , secretB :: Maybe PublicKey , secretab :: SharedSecret , secretaB :: SharedSecret , secretAb :: SharedSecret , serverHMAC :: Auth.Authenticator } -- | ConnStatus defines the progress of the handshake. data ConnStatus = StartingHandshake | AwaitingClientHello | AwaitingServerHello | AwaitingClientAuthentication | AwaitingServerAccept | HandshakeComplete deriving Show -- | Message sent between Scuttlebutt peers. -- TODO: Add encoding and processing of remaining messages data Message = ClientHello Auth.Authenticator -- | Client's HMAC PublicKey -- | Client's Ephemeral Public Key NetworkIdentifier | ServerHello Auth.Authenticator -- | Server's HMAC PublicKey -- | Server's Ephemeral Public Key NetworkIdentifier -- TODO: Can this be renamed? | ClientAuthMessage ByteString -- | Detached Signature A PublicKey -- | Client long term Public Key | ServerAccept ByteString -- | Detached Signature B -- | ConnState holds important details during the connection process. -- -- TODO: define a getter method for fields. Is it possible to get the field -- name for the error message? data ConnState = ConnState { connState :: ConnStatus , networkID :: NetworkIdentifier , clientPrivateKey :: Maybe PrivateKey , clientPublicKey :: Maybe PublicKey , clientEphemeralPrivKey :: Maybe PrivateKey , clientEphemeralPubKey :: Maybe PublicKey , clientHMAC :: Maybe Auth.Authenticator , serverPrivateKey :: Maybe PrivateKey , serverPublicKey :: Maybe PublicKey , serverEphemeralPrivKey :: Maybe PrivateKey , serverEphemeralPubKey :: Maybe PublicKey , serverHMAC :: Maybe Auth.Authenticator , sharedSecretab :: Maybe SharedSecret , sharedSecretaB :: Maybe SharedSecret , sharedSecretAb :: Maybe SharedSecret , detachedSignatureA :: Maybe ByteString , detachedSignatureB :: Maybe ByteString } -- | TODO: confirm use of default instance Default ConnState where def = ConnState { connState = StartingHandshake , networkID = "" , clientPrivateKey = def , clientPublicKey = def , clientEphemeralPrivKey = def , clientEphemeralPubKey = def , clientHMAC = def , serverPrivateKey = def , serverPublicKey = def , serverEphemeralPrivKey = def , serverEphemeralPubKey = def , serverHMAC = def , sharedSecretab = def , sharedSecretaB = def , sharedSecretAb = def , detachedSignatureA = def , detachedSignatureB = def } must :: Text -> Maybe a -> Either Text a must field = maybeToEither ("missing " <> field) -- | Create the state for initiating a Handshake given the Scuttlebutt User's key pair. newClientConnState :: NetworkIdentifier -> Identity -> PublicKey -> IO ConnState newClientConnState network clientID serverPubKey = do let clientPrivKey = Ssb.Identity.privateKey clientID let clientPubKey = Ssb.Identity.publicKey clientID (ephPrivKey, ephPubKey) <- Box.newKeypair return $ def { connState = StartingHandshake , networkID = network , clientPrivateKey = clientPrivKey , clientPublicKey = Just clientPubKey , clientEphemeralPrivKey = Just $ PrivateKey (Nacl.encode ephPrivKey) , clientEphemeralPubKey = Just $ PublicKey (Nacl.encode ephPubKey) , serverPublicKey = Just serverPubKey } -- | Create the state for initiating a Handshake given the Scuttlebutt User's key pair. newServerConnState :: NetworkIdentifier -> Identity -> IO ConnState newServerConnState network serverID = do let serverPrivKey = Ssb.Identity.privateKey serverID let serverPubKey = Ssb.Identity.publicKey serverID (ephPrivKey, ephPubKey) <- Box.newKeypair return $ def { connState = AwaitingClientHello , networkID = network , serverEphemeralPrivKey = Just $ PrivateKey (Nacl.encode ephPrivKey) , serverEphemeralPubKey = Just $ PublicKey (Nacl.encode ephPubKey) , serverPrivateKey = serverPrivKey , serverPublicKey = Just serverPubKey } -- | Create shared secrets given the Handshake's final connection state. newSharedSecrets :: ConnState -> Either Text SharedSecrets newSharedSecrets state = do ssab <- must "secret key ab" $ sharedSecretab state ssaB <- must "secret key aB" $ sharedSecretaB state ssAb <- must "secret key Ab" $ sharedSecretAb state serverHMAC' <- must "secret HMAC" $ serverHMAC (state :: ConnState) return $ SharedSecrets { network = networkID state , secreta = clientEphemeralPubKey state , secretA = clientPublicKey state , secretb = serverEphemeralPubKey state , secretB = serverPublicKey state , secretab = ssab , secretaB = ssaB , secretAb = ssAb , serverHMAC = serverHMAC' } newClientAuthMessage :: ConnState -> Either Text Message newClientAuthMessage state = do let network = networkID state cliLTPrivKey <- must "client Private Key" $ clientPrivateKey state cliLTPubKey <- must "client Public Key" $ clientPublicKey state srvLTPubKey <- must "server Public Key" $ serverPublicKey state cliEphPrivKey <- must "client Private Key" $ clientEphemeralPrivKey state srvEphPubKey <- must "server Ephemeral Public Key" $ serverEphemeralPubKey state ssab <- must "shared secret ab" $ sharedSecretab state detachedSignatureA <- newDetachedSignatureA network srvLTPubKey ssab cliLTPrivKey return $ ClientAuthMessage detachedSignatureA cliLTPubKey newClientHello :: ConnState -> Either Text Message newClientHello state = do cliEphPubKey <- maybeToEither noKeyMsg $ clientEphemeralPubKey state key <- maybeToEither badNetMsg $ Nacl.decode (networkID state) let auth = Auth.auth key (extractPublicKey cliEphPubKey) return $ ClientHello auth cliEphPubKey (networkID state) where badNetMsg = "badly formatted Network Identifier" noKeyMsg = "clientEphemeralKey required" decodeClientHello :: ConnState -> ByteString -> Either Text Message decodeClientHello state buf = do let network = networkID state let (hmacbuf, cliEphPubKey) = BS.splitAt 32 buf key <- maybeToEither badNetMsg $ Nacl.decode network auth <- maybeToEither badHMACMsg $ Nacl.decode hmacbuf let msg = cliEphPubKey if Auth.verify key auth msg then Right $ ClientHello auth (PublicKey cliEphPubKey) network else Left badVerificationMsg where badNetMsg = "badly formatted Network Identifier" badHMACMsg = "badly formatted server HMAC" badPubKeyMsg = "badly formatted server Public Key" badVerificationMsg = "verification failed" -- TODO: check if its possible to change the function depending on the return type. newServerHello :: ConnState -> Either Text Message newServerHello state = do srvEphPubKey <- maybeToEither noKeyMsg $ serverEphemeralPubKey state key <- maybeToEither badNetMsg $ Nacl.decode (networkID state) let auth = Auth.auth key (extractPublicKey srvEphPubKey) return $ ServerHello auth srvEphPubKey (networkID state) where badNetMsg = "badly formatted Network Identifier" noKeyMsg = "clientEphemeralKey required" decodeServerHello :: ConnState -> ByteString -> Either Text Message decodeServerHello state buf = do let network = networkID state let (hmacbuf, srvEphPubKey) = BS.splitAt 32 buf key <- maybeToEither badNetMsg $ Nacl.decode network auth <- maybeToEither badHMACMsg $ Nacl.decode hmacbuf let msg = srvEphPubKey if Auth.verify key auth msg then Right $ ServerHello auth (PublicKey srvEphPubKey) network else Left badVerificationMsg where badNetMsg = "badly formatted Network Identifier" badHMACMsg = "badly formatted server HMAC" badPubKeyMsg = "badly formatted server Public Key" badVerificationMsg = "verification failed" decodeClientAuthMessage :: ConnState -> ByteString -> Either Text Message decodeClientAuthMessage state buf = do let network = networkID state serverPublicKey <- must "serverPublicKey" $ serverPublicKey state sharedSecretab <- must "sharedSecretab" $ sharedSecretab state sharedSecretaB <- must "sharedSecretaB" $ sharedSecretaB state key <- naclDecode "key" $ SHA256.hash $ network <> Nacl.encode sharedSecretab <> Nacl.encode sharedSecretaB let nonce = Nacl.zero msg3 <- maybeToEither "could not open secret box" $ SecretBox.secretboxOpen key nonce buf -- TODO: Make the client auth message length a constant msg3 <- if (BS.length msg3 == 96) then (return msg3) else (Left badMessageLength) let detachedSignatureA = BS.take 64 msg3 clientLongTermPubKey <- naclDecode "client Long Term Public Key" $ BS.drop 64 msg3 let msg = (network :: ByteString) <> (extractPublicKey serverPublicKey) <> SHA256.hash (Nacl.encode sharedSecretab) if Sign.signVerifyDetached clientLongTermPubKey detachedSignatureA msg then Right state {connState = HandshakeComplete} else Left "client verification failed" return $ ClientAuthMessage detachedSignatureA (PublicKey $ Nacl.encode clientLongTermPubKey) where badMessageLength = "unexpected length of Client Authentication Message" naclDecode msg = maybeToEither msg . Nacl.decode newServerAccept :: ConnState -> Either Text Message newServerAccept state = do detachedSignatureB' <- maybeToEither noSigB (detachedSignatureB state) return $ ServerAccept detachedSignatureB' where noSigB = "detachedSignatureB required" decodeServerAccept :: ConnState -> ByteString -> Either Text Message decodeServerAccept state buf = do let network = networkID state sharedSecretab <- must "sharedSecretab" $ sharedSecretab state sharedSecretaB <- must "sharedSecretaB" $ sharedSecretaB state sharedSecretAb <- must "sharedSecretAb" $ sharedSecretAb state key <- naclDecode "key" $ SHA256.hash $ network <> Nacl.encode sharedSecretab <> Nacl.encode sharedSecretaB <> Nacl.encode sharedSecretAb let nonce = Nacl.zero detachedSignatureB <- secretBoxOpen key nonce buf return $ ServerAccept detachedSignatureB where naclDecode msg = maybeToEither ("could not decode " <> msg :: Text) . Nacl.decode secretBoxOpen key nonce msg = maybeToEither "could not open secret box" $ SecretBox.secretboxOpen key nonce msg -- | generate a signature used in the Client Authentication newDetachedSignatureA :: NetworkIdentifier -> Ssb.Identity.PublicKey -> SharedSecret -> PrivateKey -> Either Text ByteString newDetachedSignatureA network serverLongTermPubKey sharedSecretab clientLongTermPrivKey = do clientLongTermPrivKey' <- maybeToEither badCliKeyMsg $ Nacl.decode $ extractPrivateKey clientLongTermPrivKey let secretChecksum = SHA256.hash $ Nacl.encode sharedSecretab let msg = (network :: ByteString) <> extractPublicKey serverLongTermPubKey <> (secretChecksum :: ByteString) return $ Sign.signDetached clientLongTermPrivKey' msg where badSrvKeyMsg = "badly encoded long term server public key" badCliKeyMsg = "badly encoded long term client private key" calcSharedSecretab :: PrivateKey -> PublicKey -> Either Text SharedSecret calcSharedSecretab cliEphPrivKey srvEphPubKey = do cliEphPrivKey' <- maybeToEither "badly formatted client ephemeral private key" $ Nacl.decode $ extractPrivateKey cliEphPrivKey srvEphPubKey' <- maybeToEither "badly formatted server ephemeral public key" $ Nacl.decode $ extractPublicKey srvEphPubKey return $ ScalarMult.mult cliEphPrivKey' srvEphPubKey' -- | generate a signature used in the Server acknowledgement newDetachedSignatureB :: NetworkIdentifier -> ByteString -> PublicKey -> SharedSecret -> PrivateKey -> Either Text ByteString newDetachedSignatureB network detachedSignatureA clientPublicKey sharedSecretab serverPrivateKey = do key <- naclDecode badPrivkey $ extractPrivateKey serverPrivateKey let msg = (network :: ByteString) <> detachedSignatureA <> (extractPublicKey clientPublicKey) <> SHA256.hash (Nacl.encode sharedSecretab) return $ Sign.signDetached key msg where badPrivkey = "badly formatted private key" naclDecode msg = maybeToEither msg . Nacl.decode -- | Server Longterm PK should be converted to curve25519 -- Does not look like a problem given the Golang code -- TODO: Implement type conversion here clientCalcSharedSecretaB :: PrivateKey -> PublicKey -> Either Text SharedSecret clientCalcSharedSecretaB clientEphemeralSK serverLongtermPK = do cliEphPrivKey' <- maybeToEither "badly formatted client ephemeral private key" $ Nacl.decode $ extractPrivateKey clientEphemeralSK srvLTPubKey' <- maybeToEither "badly formatted server long term public key" $ Nacl.decode $ extractPublicKey serverLongtermPK curvePublicKey <- maybeToEither "badly formatted curve25519" $ Nacl.decode . Nacl.encode $ Sodium.publicKeyToCurve25519 srvLTPubKey' return $ ScalarMult.mult cliEphPrivKey' curvePublicKey serverCalcSharedSecretaB :: PrivateKey -> PublicKey -> Either Text SharedSecret serverCalcSharedSecretaB serverLongtermSK clientEphemeralPK = do srvLTPrivKey' <- maybeToEither "badly formatted server long term private key" $ Nacl.decode $ extractPrivateKey serverLongtermSK cliEphPubKey' <- maybeToEither "badly formatted client ephemeral public key" $ Nacl.decode $ extractPublicKey clientEphemeralPK curvePrivKey <- maybeToEither "badly formatted curve25519" $ Nacl.decode . Nacl.encode $ Sodium.secretKeyToCurve25519 srvLTPrivKey' return $ ScalarMult.mult curvePrivKey cliEphPubKey' calcSharedSecretAb :: PrivateKey -> PublicKey -> Either Text SharedSecret calcSharedSecretAb clientLongTermPrivKey serverEphemeralPubKey = do cliLTPrivKey' <- naclDecode "badly formatted client long term private key" $ extractPrivateKey clientLongTermPrivKey curveSecretKey <- naclDecode "badly formatted curve25519" . Nacl.encode $ Sodium.secretKeyToCurve25519 cliLTPrivKey' srvEphPubKey' <- naclDecode "badly formatted server ephemeral public key" $ extractPublicKey serverEphemeralPubKey return $ ScalarMult.mult curveSecretKey srvEphPubKey' where naclDecode msg = maybeToEither msg . Nacl.decode serverCalcSharedSecretAb :: PrivateKey -> PublicKey -> Either Text SharedSecret serverCalcSharedSecretAb serverEphemeralPrivKey clientLongTermPubKey = do srvEphPrivKey' <- naclDecode "here bad formatted server long term private key" $ extractPrivateKey serverEphemeralPrivKey cliLTPubKey' <- naclDecode "badly formatted client long term public key" $ extractPublicKey clientLongTermPubKey curvePublicKey <- naclDecode "badly formatted curve25519" . Nacl.encode $ Sodium.publicKeyToCurve25519 cliLTPubKey' return $ ScalarMult.mult srvEphPrivKey' curvePublicKey where naclDecode msg = maybeToEither msg . Nacl.decode -- | encode and serialize the message in preparation to send to peer. encode :: ConnState -> Message -> Either Text ByteString encode state msg = case msg of ClientHello auth pubKey network -> do cliEphPubKey <- maybeToEither noKeyMsg $ clientEphemeralPubKey state return $ Nacl.encode auth <> extractPublicKey cliEphPubKey where noKeyMsg = "clientEphemeralKey required" ServerHello auth pubKey network -> do return $ Nacl.encode auth <> extractPublicKey pubKey where noKeyMsg = "clientEphemeralKey required" ClientAuthMessage dSigA cliLTPubKey -> do let network = networkID state ssab <- must "shared secret ab" $ sharedSecretab state ssaB <- must "shared secret aB" $ sharedSecretaB state key <- maybeToEither badKeyMsg $ Nacl.decode $ SHA256.hash $ network <> Nacl.encode ssab <> Nacl.encode ssaB let nonce = Nacl.zero let msg = dSigA <> extractPublicKey cliLTPubKey return $ SecretBox.secretbox key nonce msg where badKeyMsg = "clientEphemeralKey required" ServerAccept detachedSignatureB -> do let network = networkID state ssab <- must "shared secret ab" $ sharedSecretab state ssaB <- must "shared secret aB" $ sharedSecretaB state ssAb <- must "shared secret Ab" $ sharedSecretAb state key <- maybeToEither badKeyMsg $ Nacl.decode $ SHA256.hash $ ((network :: ByteString) <> Nacl.encode ssab <> Nacl.encode ssaB <> Nacl.encode ssAb) let nonce = Nacl.zero let msg = detachedSignatureB return $ SecretBox.secretbox key nonce msg where badKeyMsg = "clientEphemeralKey required" -- | update the connection state and return any reponse message for the peer. -- TODO: Process secretAb process :: ConnState -> Message -> IO (Either Text (ConnState, Maybe Message)) process state (ClientHello hmac cliEphPubKey network) = do stateUpdate <- return $ do srvLTPrivKey <- must "server Private Key" $ serverPrivateKey state srvEphPrivKey <- must "server ephemeral Private Key" $ serverEphemeralPrivKey state ssab <- calcSharedSecretab srvEphPrivKey cliEphPubKey -- TODO: srvLTPubKey should be curved Process sk_to_curve25519 ssaB <- serverCalcSharedSecretaB srvLTPrivKey cliEphPubKey return $ state { connState = AwaitingClientAuthentication , clientEphemeralPubKey = Just cliEphPubKey , serverHMAC = Just hmac , sharedSecretab = Just ssab , sharedSecretaB = Just ssaB } return $ stateUpdate >>= \state' -> case newServerHello state' of Right msg' -> return $ (state', Just msg') Left err -> Left err process state (ServerHello hmac ephPubKey network) = do stateUpdate <- return $ do cliLTPrivKey <- must "client Private Key" $ clientPrivateKey state cliEphPrivKey <- must "ephemeral client Private Key" $ clientEphemeralPrivKey state srvLTPubKey <- must "server Public Key" $ serverPublicKey state ssab <- calcSharedSecretab cliEphPrivKey ephPubKey ssaB <- clientCalcSharedSecretaB cliEphPrivKey srvLTPubKey ssAb <- calcSharedSecretAb cliLTPrivKey ephPubKey return $ state { connState = AwaitingServerAccept , serverHMAC = Just hmac , serverEphemeralPubKey = Just ephPubKey , sharedSecretab = Just ssab , sharedSecretaB = Just ssaB , sharedSecretAb = Just ssAb } return $ stateUpdate >>= \state' -> case newClientAuthMessage state' of Right msg' -> return $ (state', Just msg') Left err -> Left err process state (ClientAuthMessage detachedSignatureA clientLongTermPubKey) = do stateUpdate <- return $ do let network = networkID state srvPrivKey <- must "server private key" $ serverPrivateKey state srvEphPrivKey <- must "server Long Term ephemeral private key" $ serverEphemeralPrivKey state sharedSecretAb <- serverCalcSharedSecretAb srvEphPrivKey clientLongTermPubKey sharedSecretab <- must "shared secret ab" $ sharedSecretab state detachedSignatureB <- newDetachedSignatureB network detachedSignatureA clientLongTermPubKey sharedSecretab srvPrivKey return $ state { connState = HandshakeComplete , detachedSignatureA = Just detachedSignatureA , detachedSignatureB = Just detachedSignatureB , clientPublicKey = Just clientLongTermPubKey , sharedSecretAb = Just sharedSecretAb } return $ stateUpdate >>= \state' -> case newServerAccept state' of Right msg' -> return $ (state', Just msg') Left err -> Left err process state (ServerAccept dSigB) = do stateUpdate <- return $ do let network = networkID state cliLTPrivKey <- must "client private key" $ clientPrivateKey state cliLTPubKey <- must "client public key" $ clientPublicKey state srvLTPubKey <- must "server public key" $ serverPublicKey state ssab <- must "shared secret ab" $ sharedSecretab state detachedSignatureA <- newDetachedSignatureA network srvLTPubKey ssab cliLTPrivKey keyBuf <- must "server Public Key" $ serverPublicKey state key <- maybeToEither "badly formatted public key" $ Nacl.decode $ extractPublicKey keyBuf let msg = network <> detachedSignatureA <> extractPublicKey cliLTPubKey <> SHA256.hash (Nacl.encode ssab) if Sign.signVerifyDetached key dSigB msg then Right state {connState = HandshakeComplete} else Left "server verification failed" return $ case stateUpdate of Right state' -> return (state', Nothing) Left err -> Left err -- TODO: Investigate a better way to separate network from handshake logic type ReadFn = Int -> IO (Maybe ByteString) type SendFn = ByteString -> IO () -- | readMsg decodes the next expected message from the byte stream. readMsg :: ConnState -> ReadFn -> IO (Either Text Message) readMsg state read = case connState state of AwaitingClientHello -> do mbuf <- read' challengeLength return $ do buf <- mbuf decodeClientHello state buf AwaitingServerHello -> do mbuf <- read' challengeLength return $ do buf <- mbuf decodeServerHello state buf AwaitingClientAuthentication -> do mbuf <- read' 112 return $ do buf <- mbuf decodeClientAuthMessage state buf AwaitingServerAccept -> do mbuf <- read' serverAcceptLength return $ do buf <- mbuf decodeServerAccept state buf _ -> return $ Left "unknown state" where read' len = maybeToEither "connection broken" <$> read len -- TODO: use Either instead sendMsg :: SendFn -> ConnState -> Message -> IO () sendMsg send state msg = do case encode state msg of Left err -> die err Right buf -> send buf -- | startHandshake initializes the connection with the Scuttlebutt peer -- returning the new shared secrets upon completion. startHandshake :: SendFn -> ReadFn -> NetworkIdentifier -> Identity -> PublicKey -> IO (Either Text SharedSecrets) startHandshake send recv network clientID srvPubKey = do state <- newClientConnState network clientID srvPubKey let clientHello = fromRight undefined (newClientHello state) let state' = state { connState = AwaitingServerHello } finalState <- loop state' recv (Just clientHello) return $ finalState >>= newSharedSecrets where loop :: ConnState -> ReadFn -> Maybe Message -> IO (Either Text ConnState) loop state _ Nothing = return . return $ state loop state recv (Just msg) = do sendMsg send state msg case connState state of HandshakeComplete -> return . return $ state _ -> do resp <- readMsg state recv case resp of Left err -> return $ Left err Right msg -> do res <- process state msg case res of Left err -> return $ Left ("handshake error while connecting to peer: " <> err) Right (state', msg') -> loop state' recv msg' welcomeHandshake :: SendFn -> ReadFn -> NetworkIdentifier -> Identity -> IO (Either Text SharedSecrets) welcomeHandshake send recv network serverID = do state <- newServerConnState network serverID finalState <- loop state return $ finalState >>= newSharedSecrets where loop :: ConnState -> IO (Either Text ConnState) loop state = do msg <- readMsg state recv case msg of Left err -> return $ Left err Right msg -> do res <- process state msg case res of Left err -> return $ Left $ "handshake failed: " <> err Right (state', msg') -> case msg' of Nothing -> return $ Right state' Just msg'' -> do sendMsg send state' msg'' case connState state' of HandshakeComplete -> return $ Right state' _ -> loop state'