diff options
author | Miklós Fazekas <mfazekas@szemafor.com> | 2023-05-15 10:29:32 +0200 |
---|---|---|
committer | Miklós Fazekas <mfazekas@szemafor.com> | 2023-05-15 10:39:54 +0200 |
commit | 868ed01e9f64338812c1f67e10f04e4805bdbc33 (patch) | |
tree | 4802e94b27892932b90e3917a15ef6c2c2a6c0ba | |
parent | 47deccf6ce14fceca677483c1ae4cb1227dcd562 (diff) | |
download | net-ssh-mfazekas/cacha20-poly1305.tar.gz |
POC: chacha20-poly1305 with rbnacl poly1305mfazekas/cacha20-poly1305
-rw-r--r-- | DEVELOPMENT.md | 23 | ||||
-rw-r--r-- | lib/net/ssh/transport/algorithms.rb | 18 | ||||
-rw-r--r-- | lib/net/ssh/transport/chacha20_poly1305_cipher.rb | 136 | ||||
-rw-r--r-- | lib/net/ssh/transport/cipher_factory.rb | 13 | ||||
-rw-r--r-- | lib/net/ssh/transport/identity_cipher.rb | 4 | ||||
-rw-r--r-- | lib/net/ssh/transport/packet_stream.rb | 90 | ||||
-rw-r--r-- | test/integration/test_chacha20_poly1305_cipher.rb | 45 |
7 files changed, 291 insertions, 38 deletions
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..35727b4 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,23 @@ +### Development notes + +## Building/running ssh server in debug mode + +clone the openssh server from `https://github.com/openssh/openssh-portable` + +```sh +brew install openssl +/usr/local/Cellar/openssl@3/3.1.0/bin/openssl + +autoreconf +./configure --with-ssl-dir=/usr/local/Cellar/openssl@3/3.1.0/ --with-audit=debug --enable-debug CPPFLAGS="-DDEBUG -DPACKET_DEBUG" CFLAGS="-g -O0" +make +``` + +To run server in debug mode: +```sh +echo '#' > /tmp/sshd_config +ssh-keygen -t rsa -f /tmp/ssh_host_rsa_key +# /Users/boga/Work/OSS/NetSSH/openssh-portable/sshd -p 2222 -D -d -d -d -e -f /tmp/sshd_config +/Users/boga/Work/OSS/NetSSH/openssh-portable/sshd -p 2222 -D -d -d -d -e -f /tmp/sshd_config -h /tmp/ssh_host_rsa_key + +``` diff --git a/lib/net/ssh/transport/algorithms.rb b/lib/net/ssh/transport/algorithms.rb index cc12d95..cdb63bf 100644 --- a/lib/net/ssh/transport/algorithms.rb +++ b/lib/net/ssh/transport/algorithms.rb @@ -44,7 +44,7 @@ module Net diffie-hellman-group14-sha256 diffie-hellman-group14-sha1], - encryption: %w[aes256-ctr aes192-ctr aes128-ctr], + encryption: %w[aes256-ctr aes192-ctr aes128-ctr chacha20-poly1305@openssh.com], hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com hmac-sha2-512 hmac-sha2-256 @@ -430,6 +430,15 @@ module Net sizes.max end + def expand_key(key, need_size, digester, secret, hash) + result = key + while result.size < need_size + new_part = digester.digest(secret + hash + result) + result += new_part + end + return result + 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 @@ -437,12 +446,13 @@ module Net def exchange_keys debug { "exchanging keys" } + need_bytes = kex_byte_requirement 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, + need_bytes: need_bytes, minimum_dh_bits: options[:minimum_dh_bits], logger: logger) result = algorithm.exchange_keys @@ -464,8 +474,8 @@ module Net parameters = { shared: secret, hash: hash, digester: digester } - cipher_client = CipherFactory.get(encryption_client, parameters.merge(iv: iv_client, key: key_client, encrypt: true)) - cipher_server = CipherFactory.get(encryption_server, parameters.merge(iv: iv_server, key: key_server, decrypt: true)) + cipher_client = CipherFactory.get(encryption_client, parameters.merge(iv: iv_client, key: expand_key(key_client, need_bytes, digester, secret, hash), encrypt: true)) + cipher_server = CipherFactory.get(encryption_server, parameters.merge(iv: iv_server, key: expand_key(key_server, need_bytes, digester, secret, hash), decrypt: true)) mac_client = HMAC.get(hmac_client, mac_key_client, parameters) mac_server = HMAC.get(hmac_server, mac_key_server, parameters) diff --git a/lib/net/ssh/transport/chacha20_poly1305_cipher.rb b/lib/net/ssh/transport/chacha20_poly1305_cipher.rb new file mode 100644 index 0000000..5e04a81 --- /dev/null +++ b/lib/net/ssh/transport/chacha20_poly1305_cipher.rb @@ -0,0 +1,136 @@ +require 'rbnacl' + +module Net + module SSH + module Transport + class ChaCha20Poly1305Cipher + def initialize(encrypt:, key:) + @chacha_hdr = OpenSSL::Cipher.new("chacha20") + key_len = @chacha_hdr.key_len + @chacha_main = OpenSSL::Cipher.new("chacha20") + @poly = RbNaCl::OneTimeAuths::Poly1305 + if key.size != key_len * 2 + error { "chacha20_poly1305: keylength doesn't match" } + raise "chacha20_poly1305: keylength doesn't match" + end + if encrypt + @chacha_hdr.encrypt + @chacha_main.encrypt + else + @chacha_hdr.decrypt + @chacha_main.decrypt + end + main_key = key[0...key_len] + @chacha_main.key = main_key # k2 + hdr_key = key[key_len...(2 * key_len)] + @chacha_hdr.key = hdr_key # k1 + end + + def update_cipher_mac(payload, sequence_number) + iv_data = [0, 0, 0, sequence_number].pack("NNNN") + @chacha_main.iv = iv_data + poly_key = @chacha_main.update(([0] * 32).pack('C32')) + + packet_length = payload.size + length_data = [packet_length].pack("N") + @chacha_hdr.iv = iv_data + packet = @chacha_hdr.update(length_data) + + iv_data[0] = 1.chr + @chacha_main.iv = iv_data + unencrypted_data = payload + packet += @chacha_main.update(unencrypted_data) + + packet += @poly.auth(poly_key, packet) + return packet + end + + def read_length(data, sequence_number) + iv_data = [0, 0, 0, sequence_number].pack("NNNN") + @chacha_hdr.iv = iv_data + length_data = @chacha_hdr.update(data).unpack("N").first + end + + def read_and_mac(data, mac, sequence_number) + iv_data = [0, 0, 0, sequence_number].pack("NNNN") + @chacha_main.iv = iv_data + poly_key = @chacha_main.update(([0] * 32).pack('C32')) + + iv_data[0] = 1.chr + @chacha_main.iv = iv_data + unencrypted_data = @chacha_main.update(data[4..-1]) + begin + ok = @poly.verify(poly_key, mac, data[0..-1]) + raise Net::SSH::Exception, "corrupted hmac detected #{name}" unless ok + rescue RbNaCl::BadAuthenticatorError => e + raise Net::SSH::Exception, "corrupted hmac detected #{name}" + end + return unencrypted_data + end + + def mac_length + 16 + end + + def self.key_length + 64 + end + + def block_size + 8 + end + + def name + "chacha20-poly1305@openssh.com" + end + + class << self + # A default block size of 8 is required by the SSH2 protocol. + def block_size + 8 + end + + # Returns an arbitrary integer. + def iv_len + 4 + end + + # Does nothing. Returns self. + def encrypt + self + end + + # Does nothing. Returns self. + def decrypt + self + end + + # Passes its single argument through unchanged. + def update(text) + text + end + + # Returns the empty string. + def final + "" + end + + # The name of this cipher, which is "chacha20-poly1305@openssh.com". + def name + "chacha20-poly1305@openssh.com" + end + + # Does nothing. Returns nil. + def iv=(v) + nil + end + + # Does nothing. Returns self. + def reset + self + end + end + end + end + end +end diff --git a/lib/net/ssh/transport/cipher_factory.rb b/lib/net/ssh/transport/cipher_factory.rb index 4dde239..8c61721 100644 --- a/lib/net/ssh/transport/cipher_factory.rb +++ b/lib/net/ssh/transport/cipher_factory.rb @@ -2,6 +2,7 @@ require 'openssl' require 'net/ssh/transport/ctr.rb' require 'net/ssh/transport/key_expander' require 'net/ssh/transport/identity_cipher' +require 'net/ssh/transport/chacha20_poly1305_cipher' module Net module SSH @@ -29,13 +30,17 @@ module Net 'none' => 'none' } + SSH_TO_CLASS = { + 'chacha20-poly1305@openssh.com' => Net::SSH::Transport::ChaCha20Poly1305Cipher + } + # Returns true if the underlying OpenSSL library supports the given cipher, # and false otherwise. def self.supported?(name) ossl_name = SSH_TO_OSSL[name] or raise NotImplementedError, "unimplemented cipher `#{name}'" return true if ossl_name == "none" - return OpenSSL::Cipher.ciphers.include?(ossl_name) + return SSH_TO_CLASS.key?(name) || OpenSSL::Cipher.ciphers.include?(ossl_name) end # Retrieves a new instance of the named algorithm. The new instance @@ -44,6 +49,9 @@ module Net # cipher will be put into encryption or decryption mode, based on the # value of the +encrypt+ parameter. def self.get(name, options = {}) + klass = SSH_TO_CLASS[name] + return klass.new(encrypt: options[:encrypt], key: options[:key]) unless klass.nil? + ossl_name = SSH_TO_OSSL[name] or raise NotImplementedError, "unimplemented cipher `#{name}'" return IdentityCipher if ossl_name == "none" @@ -75,6 +83,9 @@ module Net # of the tuple. # if :iv_len option is supplied the third return value will be ivlen def self.get_lengths(name, options = {}) + klass = SSH_TO_CLASS[name] + return [klass.key_length, klass.block_size] unless klass.nil? + ossl_name = SSH_TO_OSSL[name] if ossl_name.nil? || ossl_name == "none" result = [0, 0] diff --git a/lib/net/ssh/transport/identity_cipher.rb b/lib/net/ssh/transport/identity_cipher.rb index ad1a764..fdf161e 100644 --- a/lib/net/ssh/transport/identity_cipher.rb +++ b/lib/net/ssh/transport/identity_cipher.rb @@ -11,6 +11,10 @@ module Net 8 end + def key_length + 0 + end + # Returns an arbitrary integer. def iv_len 4 diff --git a/lib/net/ssh/transport/packet_stream.rb b/lib/net/ssh/transport/packet_stream.rb index a4120d4..9466096 100644 --- a/lib/net/ssh/transport/packet_stream.rb +++ b/lib/net/ssh/transport/packet_stream.rb @@ -144,31 +144,36 @@ module Net padding = Array.new(padding_length) { rand(256) }.pack("C*") - if client.hmac.etm - debug { "using encrypt-then-mac" } + if client.cipher.name == "chacha20-poly1305@openssh.com" + unencrypted_data = [padding_length, payload, padding].pack("CA*A*") + message = client.cipher.update_cipher_mac(unencrypted_data, client.sequence_number) + else + if client.hmac.etm + debug { "using encrypt-then-mac" } - # Encrypt padding_length, payload, and padding. Take MAC - # from the unencrypted packet_lenght and the encrypted - # data. - length_data = [packet_length].pack("N") + # Encrypt padding_length, payload, and padding. Take MAC + # from the unencrypted packet_lenght and the encrypted + # data. + length_data = [packet_length].pack("N") - unencrypted_data = [padding_length, payload, padding].pack("CA*A*") + unencrypted_data = [padding_length, payload, padding].pack("CA*A*") - encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher + encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher - mac_data = length_data + encrypted_data + mac_data = length_data + encrypted_data - mac = client.hmac.digest([client.sequence_number, mac_data].pack("NA*")) + mac = client.hmac.digest([client.sequence_number, mac_data].pack("NA*")) - message = mac_data + mac - else - unencrypted_data = [packet_length, padding_length, payload, padding].pack("NCA*A*") + message = mac_data + mac + else + unencrypted_data = [packet_length, padding_length, payload, padding].pack("NCA*A*") - mac = client.hmac.digest([client.sequence_number, unencrypted_data].pack("NA*")) + mac = client.hmac.digest([client.sequence_number, unencrypted_data].pack("NA*")) - encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher + encrypted_data = client.update_cipher(unencrypted_data) << client.final_cipher - message = encrypted_data + mac + message = encrypted_data + mac + end end debug { "queueing packet nr #{client.sequence_number} type #{payload.getbyte(0)} len #{packet_length}" } @@ -225,44 +230,63 @@ module Net data = read_available(minimum + aad_length) # decipher it - if server.hmac.etm - @packet_length = data.unpack("N").first + if server.cipher.name == "chacha20-poly1305@openssh.com" + @packet_length = server.cipher.read_length(data[0...4], server.sequence_number) + @packet = Net::SSH::Buffer.new() @mac_data = data - @packet = Net::SSH::Buffer.new(server.update_cipher(data[aad_length..-1])) else - @packet = Net::SSH::Buffer.new(server.update_cipher(data)) - @packet_length = @packet.read_long + if server.hmac.etm + @packet_length = data.unpack("N").first + @mac_data = data + @packet = Net::SSH::Buffer.new(server.update_cipher(data[aad_length..-1])) + else + @packet = Net::SSH::Buffer.new(server.update_cipher(data)) + @packet_length = @packet.read_long + end end end need = @packet_length + 4 - aad_length - server.block_size raise Net::SSH::Exception, "padding error, need #{need} block #{server.block_size}" if need % server.block_size != 0 - return nil if available < need + server.hmac.mac_length + if server.cipher.name == "chacha20-poly1305@openssh.com" + return nil if available < need + server.cipher.mac_length + else + return nil if available < need + server.hmac.mac_length + end if need > 0 # read the remainder of the packet and decrypt it. data = read_available(need) - @mac_data += data if server.hmac.etm - @packet.append(server.update_cipher(data)) + @mac_data += data if server.hmac.etm || (server.cipher.name == "chacha20-poly1305@openssh.com") + if server.cipher.name != "chacha20-poly1305@openssh.com" + @packet.append(server.update_cipher(data)) + end end - # get the hmac from the tail of the packet (if one exists), and - # then validate it. - real_hmac = read_available(server.hmac.mac_length) || "" + if server.cipher.name != "chacha20-poly1305@openssh.com" + # get the hmac from the tail of the packet (if one exists), and + # then validate it. + real_hmac = read_available(server.hmac.mac_length) || "" - @packet.append(server.final_cipher) - padding_length = @packet.read_byte + @packet.append(server.final_cipher) + padding_length = @packet.read_byte - payload = @packet.read(@packet_length - padding_length - 1) + payload = @packet.read(@packet_length - padding_length - 1) + - my_computed_hmac = if server.hmac.etm + my_computed_hmac = if server.hmac.etm server.hmac.digest([server.sequence_number, @mac_data].pack("NA*")) else server.hmac.digest([server.sequence_number, @packet.content].pack("NA*")) end - raise Net::SSH::Exception, "corrupted hmac detected #{server.hmac.class}" if real_hmac != my_computed_hmac - + raise Net::SSH::Exception, "corrupted hmac detected #{server.hmac.class}" if real_hmac != my_computed_hmac + else + real_hmac = read_available(server.cipher.mac_length) || "" + @packet = Net::SSH::Buffer.new(server.cipher.read_and_mac(@mac_data, real_hmac, server.sequence_number)) + padding_length = @packet.read_byte + payload = @packet.read(@packet_length - padding_length - 1) + end # try to decompress the payload, in case compression is active payload = server.decompress(payload) diff --git a/test/integration/test_chacha20_poly1305_cipher.rb b/test/integration/test_chacha20_poly1305_cipher.rb new file mode 100644 index 0000000..27fc98a --- /dev/null +++ b/test/integration/test_chacha20_poly1305_cipher.rb @@ -0,0 +1,45 @@ +require_relative 'common' +require 'fileutils' +require 'tmpdir' + +require 'net/ssh' + +require 'timeout' + +# see Vagrantfile,playbook for env. +# we're running as net_ssh_1 user password foo +# and usually connecting to net_ssh_2 user password foo2pwd +class TestChacha20Poly1305Cipher < NetSSHTest + include IntegrationTestHelpers + + def test_with_only_chacha20_cipher + config_lines = File.read('/etc/ssh/sshd_config').split("\n") + config_lines = config_lines.map do |line| + if line =~ /^Ciphers/ + "##{line}" + else + line + end + end + config_lines.push("Ciphers chacha20-poly1305@openssh.com") + + Tempfile.open('empty_kh') do |f| + f.close + start_sshd_7_or_later(config: config_lines, debug: true) do |_pid, port| + Timeout.timeout(4) do + # We have our own sshd, give it a chance to come up before + # listening. + ret = Net::SSH.start("localhost", "net_ssh_1", encryption: "chacha20-poly1305@openssh.com", password: 'foopwd', port: port, user_known_hosts_file: [f.path], verbose: :debug) do |ssh| + byebug + #assert_equal ssh.transport.algorithms.kex, "curve25519-sha256" + ssh.exec! "echo 'foo'" + end + assert_equal "foo\n", ret + rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH + sleep 0.25 + retry + end + end + end + end +end |