Skip to content

017: Setting Up The Exchange Context

Shane DeSeranno edited this page Oct 10, 2017 · 5 revisions

Before we can continue, we need to realize that the KEX process will result in an exchange context. This context is all of the selected algorithms and their keys configured. Then, when we receive the new keys message, we can switch from the old context to the new one. Then, any packet sent or received will use the new context. So, let's create an ExchangeContext class at the root of the project:

    public class ExchangeContext
    {
        public IKexAlgorithm KexAlgorithm { get; set; } = null;
        public IHostKeyAlgorithm HostKeyAlgorithm { get; set; } = null;
        public ICipher CipherClientToServer { get; set; } = new NoCipher();
        public ICipher CipherServerToClient { get; set; } = new NoCipher();
        public IMACAlgorithm MACAlgorithmClientToServer { get; set; } = null;
        public IMACAlgorithm MACAlgorithmServerToClient { get; set; } = null;
        public ICompression CompressionClientToServer { get; set; } = new NoCompression();
        public ICompression CompressionServerToClient { get; set; } = new NoCompression();
    }

We default the ciphers to NoCipher and the compressions to NoCompression. Now we can add our active and pending exchange contexts to the Client class:

    public class Client
    {
        ...
        private KexInit m_KexInitClientToServer = null;

        private ExchangeContext m_ActiveExchangeContext = new ExchangeContext();
        private ExchangeContext m_PendingExchangeContext = new ExchangeContext();
        ...
    }

Notice, we added the client's KexInit packet they sent us. This is actually needed for part of the KEX process later. The active exchange context is the one we use for sending and receiving. The pending is the one we are currently building via the KEX. Now we need to write a handler for our KexInit packet so we can setup our pending context. I used a fancy C# feature that allows for runtime binding to determine which method to call:

       private void HandlePacket(Packet packet)
        {
            try
            {
                HandleSpecificPacket((dynamic)packet);
            }
            catch(RuntimeBinderException)
            {
                // TODO: Send an SSH_MSG_UNIMPLEMENTED if we get here
            }
        }


        private void HandleSpecificPacket(KexInit packet)
        {
           ...
        }

Now we can actually start processing the KexInit packet they sent us. We need to look at the choices and pick the best (first) match in each set that we have. I decided it would be clean to put methods on the KexInit packet to handle this logic so it is all in the same small class:

    public class KexInit : Packet
    {
        ...
        public IKexAlgorithm PickKexAlgorithm()
        {
            foreach (string algo in this.KexAlgorithms)
            {
                IKexAlgorithm selectedAlgo = Server.GetType<IKexAlgorithm>(Server.SupportedKexAlgorithms, algo);
                if (selectedAlgo != null)
                {
                    return selectedAlgo;
                }
            }

            // If no algorithm satisfying all these conditions can be found, the
            // connection fails, and both sides MUST disconnect.
            throw new NotSupportedException("Could not find a shared Kex Algorithm");
        }

        public IHostKeyAlgorithm PickHostKeyAlgorithm()
        {
            foreach (string algo in this.ServerHostKeyAlgorithms)
            {
                IHostKeyAlgorithm selectedAlgo = Server.GetType<IHostKeyAlgorithm>(Server.SupportedHostKeyAlgorithms, algo);
                if (selectedAlgo != null)
                {
                    return selectedAlgo;
                }
            }

            // If no algorithm satisfying all these conditions can be found, the
            // connection fails, and both sides MUST disconnect.
            throw new NotSupportedException("Could not find a shared Host Key Algorithm");
        }

        public ICipher PickCipherClientToServer()
        {
            foreach (string algo in this.EncryptionAlgorithmsClientToServer)
            {
                ICipher selectedCipher = Server.GetType>ICipher<(Server.SupportedCiphers, algo);
                if (selectedCipher != null)
                {
                    return selectedCipher;
                }
            }

            // If no algorithm satisfying all these conditions can be found, the
            // connection fails, and both sides MUST disconnect.
            throw new NotSupportedException("Could not find a shared Client-To-Server Cipher Algorithm");
        }

        public ICipher PickCipherServerToClient()
        {
            foreach (string algo in this.EncryptionAlgorithmsServerToClient)
            {
                ICipher selectedCipher = Server.GetType<ICipher>(Server.SupportedCiphers, algo);
                if (selectedCipher != null)
                {
                    return selectedCipher;
                }
            }

            // If no algorithm satisfying all these conditions can be found, the
            // connection fails, and both sides MUST disconnect.
            throw new NotSupportedException("Could not find a shared Server-To-Client Cipher Algorithm");
        }

        public IMACAlgorithm PickMACAlgorithmClientToServer()
        {
            foreach (string algo in this.MacAlgorithmsClientToServer)
            {
                IMACAlgorithm selectedAlgo = Server.GetType<IMACAlgorithm>(Server.SupportedMACAlgorithms, algo);
                if (selectedAlgo != null)
                {
                    return selectedAlgo;
                }
            }

            // If no algorithm satisfying all these conditions can be found, the
            // connection fails, and both sides MUST disconnect.
            throw new NotSupportedException("Could not find a shared Client-To-Server MAC Algorithm");
        }

        public IMACAlgorithm PickMACAlgorithmServerToClient()
        {
            foreach (string algo in this.MacAlgorithmsServerToClient)
            {
                IMACAlgorithm selectedAlgo = Server.GetType<IMACAlgorithm>(Server.SupportedMACAlgorithms, algo);
                if (selectedAlgo != null)
                {
                    return selectedAlgo;
                }
            }

            // If no algorithm satisfying all these conditions can be found, the
            // connection fails, and both sides MUST disconnect.
            throw new NotSupportedException("Could not find a shared Server-To-Client MAC Algorithm");
        }

        public ICompression PickCompressionAlgorithmClientToServer()
        {
            foreach (string algo in this.CompressionAlgorithmsClientToServer)
            {
                ICompression selectedAlgo = Server.GetType<ICompression>(Server.SupportedCompressions, algo);
                if (selectedAlgo != null)
                {
                    return selectedAlgo;
                }
            }

            // If no algorithm satisfying all these conditions can be found, the
            // connection fails, and both sides MUST disconnect.
            throw new NotSupportedException("Could not find a shared Client-To-Server Compresion Algorithm");
        }

        public ICompression PickCompressionAlgorithmServerToClient()
        {
            foreach (string algo in this.CompressionAlgorithmsServerToClient)
            {
                ICompression selectedAlgo = Server.GetType<ICompression>(Server.SupportedCompressions, algo);
                if (selectedAlgo != null)
                {
                    return selectedAlgo;
                }
            }

            // If no algorithm satisfying all these conditions can be found, the
            // connection fails, and both sides MUST disconnect.
            throw new NotSupportedException("Could not find a shared Server-To-Client Compresion Algorithm");
        }
    }

This basically just looks at the Server's supported lists and finds the first match to the client's supported list. If it finds one, then it ask the server to create the selected one. We wrote that helper a while ago to ensure any keys were imported! Next, we need to update our KexInit handler to use these methods:

        private void HandleSpecificPacket(KexInit packet)
        {
            m_Logger.LogDebug("Received KexInit");

            if (m_PendingExchangeContext == null)
            {
                m_Logger.LogDebug("Re-exchanging keys!");
                m_PendingExchangeContext = new ExchangeContext();
                Send(m_KexInitServerToClient);
            }

            m_KexInitClientToServer = packet;

            m_PendingExchangeContext.KexAlgorithm = packet.PickKexAlgorithm();
            m_PendingExchangeContext.HostKeyAlgorithm = packet.PickHostKeyAlgorithm();
            m_PendingExchangeContext.CipherClientToServer = packet.PickCipherClientToServer();
            m_PendingExchangeContext.CipherServerToClient = packet.PickCipherServerToClient();
            m_PendingExchangeContext.MACAlgorithmClientToServer = packet.PickMACAlgorithmClientToServer();
            m_PendingExchangeContext.MACAlgorithmServerToClient = packet.PickMACAlgorithmServerToClient();
            m_PendingExchangeContext.CompressionClientToServer = packet.PickCompressionAlgorithmClientToServer();
            m_PendingExchangeContext.CompressionServerToClient = packet.PickCompressionAlgorithmServerToClient();

            m_Logger.LogDebug($"Selected KexAlgorithm: {m_PendingExchangeContext.KexAlgorithm.Name}");
            m_Logger.LogDebug($"Selected HostKeyAlgorithm: {m_PendingExchangeContext.HostKeyAlgorithm.Name}");
            m_Logger.LogDebug($"Selected CipherClientToServer: {m_PendingExchangeContext.CipherClientToServer.Name}");
            m_Logger.LogDebug($"Selected CipherServerToClient: {m_PendingExchangeContext.CipherServerToClient.Name}");
            m_Logger.LogDebug($"Selected MACAlgorithmClientToServer: {m_PendingExchangeContext.MACAlgorithmClientToServer.Name}");
            m_Logger.LogDebug($"Selected MACAlgorithmServerToClient: {m_PendingExchangeContext.MACAlgorithmServerToClient.Name}");
            m_Logger.LogDebug($"Selected CompressionClientToServer: {m_PendingExchangeContext.CompressionClientToServer.Name}");
            m_Logger.LogDebug($"Selected CompressionServerToClient: {m_PendingExchangeContext.CompressionServerToClient.Name}");
        }

It basically just calls each of these helper methods and uses them to set the pending exchange context values. Now, if we run the server and connect with OpenSSH we see:

...
dbug: 127.0.0.1:1766[0]
      Received KexInit
dbug: 127.0.0.1:1766[0]
      Selected KexAlgorithm: diffie-hellman-group14-sha1
dbug: 127.0.0.1:1766[0]
      Selected HostKeyAlgorithm: ssh-rsa
dbug: 127.0.0.1:1766[0]
      Selected CipherClientToServer: 3des-cbc
dbug: 127.0.0.1:1766[0]
      Selected CipherServerToClient: 3des-cbc
dbug: 127.0.0.1:1766[0]
      Selected MACAlgorithmClientToServer: hmac-sha1
dbug: 127.0.0.1:1766[0]
      Selected MACAlgorithmServerToClient: hmac-sha1
dbug: 127.0.0.1:1766[0]
      Selected CompressionClientToServer: none
dbug: 127.0.0.1:1766[0]
      Selected CompressionServerToClient: none

Clearly our new handler has been called and it has selected all the same algorithms that OpenSSH had! We are now ready to handle the next packet from the client! First, the code at this point is tagged Exchange_Context_And_KexInit. In the next section, we'll handle SSH_MSG_KEXDH_INIT