require 'net/ssh/buffer' require 'net/ssh/known_hosts' require 'net/ssh/loggable' require 'net/ssh/transport/cipher_factory' require 'net/ssh/transport/constants' require 'net/ssh/transport/hmac' require 'net/ssh/transport/kex' require 'net/ssh/transport/server_version' module Net; module SSH; module Transport # Implements the higher-level logic behind an SSH key-exchange. It handles # both the initial exchange, as well as subsequent re-exchanges (as needed). # It also encapsulates the negotiation of the algorithms, and provides a # single point of access to the negotiated algorithms. # # You will never instantiate or reference this directly. It is used # internally by the transport layer. class Algorithms include Constants, Loggable # Define the default algorithms, in order of preference, supported by # Net::SSH. ALGORITHMS = { :host_key => %w(ssh-rsa ssh-dss), :kex => %w(diffie-hellman-group-exchange-sha1 diffie-hellman-group1-sha1), :encryption => %w(aes128-cbc 3des-cbc blowfish-cbc cast128-cbc aes192-cbc aes256-cbc rijndael-cbc@lysator.liu.se idea-cbc none), :hmac => %w(hmac-sha1 hmac-md5 hmac-sha1-96 hmac-md5-96 none), :compression => %w(none zlib@openssh.com zlib), :language => %w() } # The underlying transport layer session that supports this object attr_reader :session # The hash of options used to initialize this object attr_reader :options # The kex algorithm to use settled on between the client and server. attr_reader :kex # The type of host key that will be used for this session. attr_reader :host_key # The type of the cipher to use to encrypt packets sent from the client to # the server. attr_reader :encryption_client # The type of the cipher to use to decrypt packets arriving from the server. attr_reader :encryption_server # The type of HMAC to use to sign packets sent by the client. attr_reader :hmac_client # The type of HMAC to use to validate packets arriving from the server. attr_reader :hmac_server # The type of compression to use to compress packets being sent by the client. attr_reader :compression_client # The type of compression to use to decompress packets arriving from the server. attr_reader :compression_server # The language that will be used in messages sent by the client. attr_reader :language_client # The language that will be used in messages sent from the server. attr_reader :language_server # The hash of algorithms preferred by the client, which will be told to # the server during algorithm negotiation. attr_reader :algorithms # The session-id for this session, as decided during the initial key exchange. attr_reader :session_id # Returns true if the given packet can be processed during a key-exchange. def self.allowed_packet?(packet) ( 1.. 4).include?(packet.type) || ( 6..19).include?(packet.type) || (21..49).include?(packet.type) end # Instantiates a new Algorithms object, and prepares the hash of preferred # algorithms based on the options parameter and the ALGORITHMS constant. def initialize(session, options={}) @session = session @logger = session.logger @options = options @algorithms = {} @pending = @initialized = false @client_packet = @server_packet = nil prepare_preferred_algorithms! end # Request a rekey operation. This will return immediately, and does not # actually perform the rekey operation. It does cause the session to change # state, however--until the key exchange finishes, no new packets will be # processed. def rekey! @client_packet = @server_packet = nil @initialized = false send_kexinit end # Called by the transport layer when a KEXINIT packet is recieved, indicating # that the server wants to exchange keys. This can be spontaneous, or it # can be in response to a client-initiated rekey request (see #rekey!). Either # way, this will block until the key exchange completes. def accept_kexinit(packet) info { "got KEXINIT from server" } @server_data = parse_server_algorithm_packet(packet) @server_packet = @server_data[:raw] if !pending? send_kexinit else proceed! end end # A convenience method for accessing the list of preferred types for a # specific algorithm (see #algorithms). def [](key) algorithms[key] end # Returns +true+ if a key-exchange is pending. This will be true from the # moment either the client or server requests the key exchange, until the # exchange completes. While an exchange is pending, only a limited number # of packets are allowed, so event processing essentially stops during this # period. def pending? @pending end # Returns true if no exchange is pending, and otherwise returns true or # false depending on whether the given packet is of a type that is allowed # during a key exchange. def allow?(packet) !pending? || Algorithms.allowed_packet?(packet) end # Returns true if the algorithms have been negotiated at all. def initialized? @initialized end private # Sends a KEXINIT packet to the server. If a server KEXINIT has already # been received, this will then invoke #proceed! to proceed with the key # exchange, otherwise it returns immediately (but sets the object to the # pending state). def send_kexinit info { "sending KEXINIT" } @pending = true packet = build_client_algorithm_packet @client_packet = packet.to_s session.send_message(packet) proceed! if @server_packet end # After both client and server have sent their KEXINIT packets, this # will do the algorithm negotiation and key exchange. Once both finish, # the object leaves the pending state and the method returns. def proceed! info { "negotiating algorithms" } negotiate_algorithms exchange_keys @pending = false end # Prepares the list of preferred algorithms, based on the options hash # that was given when the object was constructed, and the ALGORITHMS # constant. Also, when determining the host_key type to use, the known # hosts files are examined to see if the host has ever sent a host_key # before, and if so, that key type is used as the preferred type for # communicating with this server. def prepare_preferred_algorithms! options[:compression] = %w(zlib@openssh.com zlib) if options[:compression] == true ALGORITHMS.each do |algorithm, list| algorithms[algorithm] = list.dup # apply the preferred algorithm order, if any if options[algorithm] algorithms[algorithm] = Array(options[algorithm]).compact.uniq invalid = algorithms[algorithm].detect { |name| !ALGORITHMS[algorithm].include?(name) } raise NotImplementedError, "unsupported #{algorithm} algorithm: `#{invalid}'" if invalid # make sure all of our supported algorithms are tacked onto the # end, so that if the user tries to give a list of which none are # supported, we can still proceed. list.each { |name| algorithms[algorithm] << name unless algorithms[algorithm].include?(name) } end end # for convention, make sure our list has the same keys as the server # list algorithms[:encryption_client ] = algorithms[:encryption_server ] = algorithms[:encryption] algorithms[:hmac_client ] = algorithms[:hmac_server ] = algorithms[:hmac] algorithms[:compression_client] = algorithms[:compression_server] = algorithms[:compression] algorithms[:language_client ] = algorithms[:language_server ] = algorithms[:language] if !options.key?(:host_key) # make sure the host keys are specified in preference order, where any # existing known key for the host has preference. existing_keys = KnownHosts.search_for(options[:host_key_alias] || session.host_as_string, options) host_keys = existing_keys.map { |key| key.ssh_type }.uniq algorithms[:host_key].each do |name| host_keys << name unless host_keys.include?(name) end algorithms[:host_key] = host_keys end end # Parses a KEXINIT packet from the server. def parse_server_algorithm_packet(packet) data = { :raw => packet.content } packet.read(16) # skip the cookie value data[:kex] = packet.read_string.split(/,/) data[:host_key] = packet.read_string.split(/,/) data[:encryption_client] = packet.read_string.split(/,/) data[:encryption_server] = packet.read_string.split(/,/) data[:hmac_client] = packet.read_string.split(/,/) data[:hmac_server] = packet.read_string.split(/,/) data[:compression_client] = packet.read_string.split(/,/) data[:compression_server] = packet.read_string.split(/,/) data[:language_client] = packet.read_string.split(/,/) data[:language_server] = packet.read_string.split(/,/) # TODO: if first_kex_packet_follows, we need to try to skip the # actual kexinit stuff and try to guess what the server is doing... # need to read more about this scenario. first_kex_packet_follows = packet.read_bool return data end # Given the #algorithms map of preferred algorithm types, this constructs # a KEXINIT packet to send to the server. It does not actually send it, # it simply builds the packet and returns it. def build_client_algorithm_packet kex = algorithms[:kex ].join(",") host_key = algorithms[:host_key ].join(",") encryption = algorithms[:encryption ].join(",") hmac = algorithms[:hmac ].join(",") compression = algorithms[:compression].join(",") language = algorithms[:language ].join(",") Net::SSH::Buffer.from(:byte, KEXINIT, :long, [rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF), rand(0xFFFFFFFF)], :string, [kex, host_key, encryption, encryption, hmac, hmac], :string, [compression, compression, language, language], :bool, false, :long, 0) end # Given the parsed server KEX packet, and the client's preferred algorithm # lists in #algorithms, determine which preferred algorithms each has # in common and set those as the selected algorithms. If, for any algorithm, # no type can be settled on, an exception is raised. def negotiate_algorithms @kex = negotiate(:kex) @host_key = negotiate(:host_key) @encryption_client = negotiate(:encryption_client) @encryption_server = negotiate(:encryption_server) @hmac_client = negotiate(:hmac_client) @hmac_server = negotiate(:hmac_server) @compression_client = negotiate(:compression_client) @compression_server = negotiate(:compression_server) @language_client = negotiate(:language_client) rescue "" @language_server = negotiate(:language_server) rescue "" debug do "negotiated:\n" + [:kex, :host_key, :encryption_server, :encryption_client, :hmac_client, :hmac_server, :compression_client, :compression_server, :language_client, :language_server].map do |key| "* #{key}: #{instance_variable_get("@#{key}")}" end.join("\n") end end # Negotiates a single algorithm based on the preferences reported by the # server and those set by the client. This is called by # #negotiate_algorithms. def negotiate(algorithm) match = self[algorithm].find { |item| @server_data[algorithm].include?(item) } if match.nil? raise Net::SSH::Exception, "could not settle on #{algorithm} algorithm" end return match end # Considers the sizes of the keys and block-sizes for the selected ciphers, # and the lengths of the hmacs, and returns the largest as the byte requirement # for the key-exchange algorithm. def kex_byte_requirement sizes = [8] # require at least 8 bytes sizes.concat(CipherFactory.get_lengths(encryption_client)) sizes.concat(CipherFactory.get_lengths(encryption_server)) sizes << HMAC.key_length(hmac_client) sizes << HMAC.key_length(hmac_server) sizes.max end # Instantiates one of the Transport::Kex classes (based on the negotiated # kex algorithm), and uses it to exchange keys. Then, the ciphers and # HMACs are initialized and fed to the transport layer, to be used in # further communication with the server. def exchange_keys debug { "exchanging keys" } algorithm = Kex::MAP[kex].new(self, session, :client_version_string => Net::SSH::Transport::ServerVersion::PROTO_VERSION, :server_version_string => session.server_version.version, :server_algorithm_packet => @server_packet, :client_algorithm_packet => @client_packet, :need_bytes => kex_byte_requirement, :logger => logger) result = algorithm.exchange_keys secret = result[:shared_secret].to_ssh hash = result[:session_id] digester = result[:hashing_algorithm] @session_id ||= hash key = Proc.new { |salt| digester.digest(secret + hash + salt + @session_id) } iv_client = key["A"] iv_server = key["B"] key_client = key["C"] key_server = key["D"] mac_key_client = key["E"] mac_key_server = key["F"] parameters = { :iv => iv_client, :key => key_client, :shared => secret, :hash => hash, :digester => digester } cipher_client = CipherFactory.get(encryption_client, parameters.merge(:encrypt => true)) cipher_server = CipherFactory.get(encryption_server, parameters.merge(:iv => iv_server, :key => key_server, :decrypt => true)) mac_client = HMAC.get(hmac_client, mac_key_client) mac_server = HMAC.get(hmac_server, mac_key_server) session.configure_client :cipher => cipher_client, :hmac => mac_client, :compression => normalize_compression_name(compression_client), :compression_level => options[:compression_level], :rekey_limit => options[:rekey_limit], :max_packets => options[:rekey_packet_limit], :max_blocks => options[:rekey_blocks_limit] session.configure_server :cipher => cipher_server, :hmac => mac_server, :compression => normalize_compression_name(compression_server), :rekey_limit => options[:rekey_limit], :max_packets => options[:rekey_packet_limit], :max_blocks => options[:rekey_blocks_limit] @initialized = true end # Given the SSH name for some compression algorithm, return a normalized # name as a symbol. def normalize_compression_name(name) case name when "none" then false when "zlib" then :standard when "zlib@openssh.com" then :delayed else raise ArgumentError, "unknown compression type `#{name}'" end end end end; end; end