From a280273ecb8fd0a4c959977020b50a58a7ee454b Mon Sep 17 00:00:00 2001 From: "benjamin.jessop" Date: Wed, 13 Mar 2024 13:58:19 +1300 Subject: [PATCH] feat: Add support for aes(128|256)gcm Close https://github.com/net-ssh/net-ssh/issues/834 --- .rubocop_todo.yml | 2 +- lib/net/ssh/transport/aes128_gcm.rb | 40 +++++ lib/net/ssh/transport/aes256_gcm.rb | 40 +++++ lib/net/ssh/transport/algorithms.rb | 9 +- lib/net/ssh/transport/cipher_factory.rb | 16 +- lib/net/ssh/transport/gcm_cipher.rb | 207 ++++++++++++++++++++++++ lib/net/ssh/transport/hmac/abstract.rb | 16 ++ lib/net/ssh/transport/packet_stream.rb | 6 +- lib/net/ssh/transport/state.rb | 2 +- test/integration/README.md | 2 +- test/integration/playbook.yml | 2 + test/integration/test_gcm_cipher.rb | 53 ++++++ test/transport/test_algorithms.rb | 18 +-- test/transport/test_cipher_factory.rb | 43 ++++- test/transport/test_packet_stream.rb | 18 ++- 15 files changed, 444 insertions(+), 30 deletions(-) create mode 100644 lib/net/ssh/transport/aes128_gcm.rb create mode 100644 lib/net/ssh/transport/aes256_gcm.rb create mode 100644 lib/net/ssh/transport/gcm_cipher.rb create mode 100644 test/integration/test_gcm_cipher.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 12dda331e..57ca9f4c3 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -235,7 +235,7 @@ Lint/UselessTimes: # Offense count: 205 # Configuration parameters: IgnoredMethods, CountRepeatedAttributes. Metrics/AbcSize: - Max: 74 + Max: 75 # Offense count: 16 # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. diff --git a/lib/net/ssh/transport/aes128_gcm.rb b/lib/net/ssh/transport/aes128_gcm.rb new file mode 100644 index 000000000..736e17344 --- /dev/null +++ b/lib/net/ssh/transport/aes128_gcm.rb @@ -0,0 +1,40 @@ +require 'net/ssh/transport/hmac/abstract' +require 'net/ssh/transport/gcm_cipher' + +module Net::SSH::Transport + ## Implements the aes128-gcm@openssh cipher + class AES128_GCM + extend ::Net::SSH::Transport::GCMCipher + + ## Implicit HMAC, do need to do anything + class ImplicitHMac < ::Net::SSH::Transport::HMAC::Abstract + def aead + true + end + + def key_length + 16 + end + end + + def implicit_mac + ImplicitHMac.new + end + + def algo_name + 'aes-128-gcm' + end + + def name + 'aes128-gcm@openssh.com' + end + + # + # --- RFC 5647 --- + # K_LEN AES key length 16 octets + # + def self.key_length + 16 + end + end +end diff --git a/lib/net/ssh/transport/aes256_gcm.rb b/lib/net/ssh/transport/aes256_gcm.rb new file mode 100644 index 000000000..1fa7a131d --- /dev/null +++ b/lib/net/ssh/transport/aes256_gcm.rb @@ -0,0 +1,40 @@ +require 'net/ssh/transport/hmac/abstract' +require 'net/ssh/transport/gcm_cipher' + +module Net::SSH::Transport + ## Implements the aes256-gcm@openssh cipher + class AES256_GCM + extend ::Net::SSH::Transport::GCMCipher + + ## Implicit HMAC, do need to do anything + class ImplicitHMac < ::Net::SSH::Transport::HMAC::Abstract + def aead + true + end + + def key_length + 32 + end + end + + def implicit_mac + ImplicitHMac.new + end + + def algo_name + 'aes-256-gcm' + end + + def name + 'aes256-gcm@openssh.com' + end + + # + # --- RFC 5647 --- + # K_LEN AES key length 32 octets + # + def self.key_length + 32 + end + end +end diff --git a/lib/net/ssh/transport/algorithms.rb b/lib/net/ssh/transport/algorithms.rb index 9631775a2..0d1c6eb94 100644 --- a/lib/net/ssh/transport/algorithms.rb +++ b/lib/net/ssh/transport/algorithms.rb @@ -44,7 +44,11 @@ class Algorithms diffie-hellman-group14-sha256 diffie-hellman-group14-sha1], - encryption: %w[aes256-ctr aes192-ctr aes128-ctr], + encryption: %w[aes256-ctr + aes192-ctr + aes128-ctr + aes256-gcm@openssh.com + aes128-gcm@openssh.com], hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com hmac-sha2-512 hmac-sha2-256 @@ -492,6 +496,9 @@ def exchange_keys HMAC.get(hmac_server, mac_key_server, parameters) end + cipher_client.nonce = iv_client if mac_client.respond_to?(:aead) && mac_client.aead + cipher_server.nonce = iv_server if mac_server.respond_to?(:aead) && mac_client.aead + session.configure_client cipher: cipher_client, hmac: mac_client, compression: normalize_compression_name(compression_client), compression_level: options[:compression_level], diff --git a/lib/net/ssh/transport/cipher_factory.rb b/lib/net/ssh/transport/cipher_factory.rb index c8eb9225c..9fc52ea31 100644 --- a/lib/net/ssh/transport/cipher_factory.rb +++ b/lib/net/ssh/transport/cipher_factory.rb @@ -1,5 +1,7 @@ require 'openssl' require 'net/ssh/transport/ctr.rb' +require 'net/ssh/transport/aes128_gcm' +require 'net/ssh/transport/aes256_gcm' require 'net/ssh/transport/key_expander' require 'net/ssh/transport/identity_cipher' require 'net/ssh/transport/chacha20_poly1305_cipher_loader' @@ -31,15 +33,15 @@ class CipherFactory 'none' => 'none' } - SSH_TO_CLASS = + SSH_TO_CLASS = { + 'aes256-gcm@openssh.com' => Net::SSH::Transport::AES256_GCM, + 'aes128-gcm@openssh.com' => Net::SSH::Transport::AES128_GCM + }.tap do |hash| if Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED - { - 'chacha20-poly1305@openssh.com' => Net::SSH::Transport::ChaCha20Poly1305Cipher - } - else - { - } + hash['chacha20-poly1305@openssh.com'] = + Net::SSH::Transport::ChaCha20Poly1305Cipher end + end # Returns true if the underlying OpenSSL library supports the given cipher, # and false otherwise. diff --git a/lib/net/ssh/transport/gcm_cipher.rb b/lib/net/ssh/transport/gcm_cipher.rb new file mode 100644 index 000000000..6ae7d6ff9 --- /dev/null +++ b/lib/net/ssh/transport/gcm_cipher.rb @@ -0,0 +1,207 @@ +require 'net/ssh/loggable' + +module Net + module SSH + module Transport + ## Extension module for aes(128|256)gcm ciphers + module GCMCipher + # rubocop:disable Metrics/AbcSize + def self.extended(orig) + # rubocop:disable Metrics/BlockLength + orig.class_eval do + include Net::SSH::Loggable + + attr_reader :cipher + attr_reader :key + attr_accessor :nonce + + # + # Semantically gcm cipher supplies the OpenSSL iv interface with a nonce + # as it is not randomly generated due to being supplied from a counter. + # The RFC's use IV and nonce interchangeably. + # + def initialize(encrypt:, key:) + @cipher = OpenSSL::Cipher.new(algo_name) + @key = key + key_len = @cipher.key_len + if key.size != key_len + error_message = "#{cipher_name}: keylength does not match" + error { error_message } + raise error_message + end + encrypt ? @cipher.encrypt : @cipher.decrypt + @cipher.key = key + + @nonce = { + fixed: nil, + invocation_counter: 0 + } + end + + def update_cipher_mac(payload, _sequence_number) + # + # --- RFC 5647 7.3 --- + # When using AES-GCM with secure shell, the packet_length field is to + # be treated as additional authenticated data, not as plaintext. + # + length_data = [payload.bytesize].pack('N') + + cipher.auth_data = length_data + + encrypted_data = cipher.update(payload) << cipher.final + + mac = cipher.auth_tag + + incr_nonce + length_data + encrypted_data + mac + end + + # + # --- RFC 5647 --- + # uint32 packet_length; // 0 <= packet_length < 2^32 + # + def read_length(data, _sequence_number) + data.unpack1('N') + end + + # + # --- RFC 5647 --- + # In AES-GCM secure shell, the inputs to the authenticated encryption + # are: + # PT (Plain Text) + # byte padding_length; // 4 <= padding_length < 256 + # byte[n1] payload; // n1 = packet_length-padding_length-1 + # byte[n2] random_padding; // n2 = padding_length + # AAD (Additional Authenticated Data) + # uint32 packet_length; // 0 <= packet_length < 2^32 + # IV (Initialization Vector) + # As described in section 7.1. + # BK (Block Cipher Key) + # The appropriate Encryption Key formed during the Key Exchange. + # + def read_and_mac(data, mac, _sequence_number) + # The authentication tag will be placed in the MAC field at the end of the packet + + # OpenSSL does not verify auth tag length + # GCM mode allows arbitrary sizes for the auth_tag up to 128 bytes and a single + # byte allows authentication to pass. If single byte auth tags are possible + # an attacker would require no more than 256 attempts to forge a valid tag. + # + raise 'incorrect auth_tag length' unless mac.to_s.length == mac_length + + packet_length = data.unpack1('N') + + cipher.auth_tag = mac.to_s + cipher.auth_data = [packet_length].pack('N') + + result = cipher.update(data[4...]) << cipher.final + incr_nonce + result + end + + def mac_length + 16 + end + + def block_size + 16 + end + + def self.block_size + 16 + end + + # + # --- RFC 5647 --- + # N_MIN minimum nonce (IV) length 12 octets + # N_MAX maximum nonce (IV) length 12 octets + # + def iv_len + 12 + end + + # + # --- RFC 5288 --- + # Each value of the nonce_explicit MUST be distinct for each distinct + # invocation of the GCM encrypt function for any fixed key. Failure to + # meet this uniqueness requirement can significantly degrade security. + # The nonce_explicit MAY be the 64-bit sequence number. + # + # --- RFC 5116 --- + # (2.1) Applications that can generate distinct nonces SHOULD use the nonce + # formation method defined in Section 3.2, and MAY use any + # other method that meets the uniqueness requirement. + # + # (3.2) The following method to construct nonces is RECOMMENDED. + # + # <- variable -> <- variable -> + # - - - - - - - - - - - - - - + # | fixed | counter | + # + # Initial octets consist of a fixed field and final octets consist of a + # Counter field. Implementations SHOULD support 12-octet nonces in which + # the Counter field is four octets long. + # The Counter fields of successive nonces form a monotonically increasing + # sequence, when those fields are regarded as unsignd integers in network + # byte order. + # The Counter part SHOULD be equal to zero for the first nonce and increment + # by one for each successive nonce that is generated. + # The Fixed field MUST remain constant for all nonces that are generated for + # a given encryption device. + # + # --- RFC 5647 --- + # The invocation field is treated as a 64-bit integer and is increment after + # each invocation of AES-GCM to process a binary packet. + # AES-GCM produces a keystream in blocks of 16-octets that is used to + # encrypt the plaintext. This keystream is produced by encrypting the + # following 16-octet data structure: + # + # uint32 fixed; // 4 octets + # uint64 invocation_counter; // 8 octets + # unit32 block_counter; // 4 octets + # + # The block_counter is initially set to one (1) and increment as each block + # of key is produced. + # + # The reader is reminded that SSH requires that the data to be encrypted + # MUST be padded out to a multiple of the block size (16-octets for AES-GCM). + # + def incr_nonce + return if nonce[:fixed].nil? + + nonce[:invocation_counter] = [nonce[:invocation_counter].to_s.unpack1('B*').to_i(2) + 1].pack('Q>*') + + apply_nonce + end + + def nonce=(iv_s) + return if nonce[:fixed] + + nonce[:fixed] = iv_s[0...4] + nonce[:invocation_counter] = iv_s[4...12] + + apply_nonce + end + + def apply_nonce + cipher.iv = "#{nonce[:fixed]}#{nonce[:invocation_counter]}" + end + + # + # --- RFC 5647 --- + # If AES-GCM is selected as the encryption algorithm for a given + # tunnel, AES-GCM MUST also be selected as the Message Authentication + # Code (MAC) algorithm. Conversely, if AES-GCM is selected as the MAC + # algorithm, it MUST also be selected as the encryption algorithm. + # + def implicit_mac? + true + end + end + end + # rubocop:enable Metrics/BlockLength + end + # rubocop:enable Metrics/AbcSize + end + end +end diff --git a/lib/net/ssh/transport/hmac/abstract.rb b/lib/net/ssh/transport/hmac/abstract.rb index 8bc2a56bb..d04f012e9 100644 --- a/lib/net/ssh/transport/hmac/abstract.rb +++ b/lib/net/ssh/transport/hmac/abstract.rb @@ -8,6 +8,18 @@ module HMAC # The base class of all OpenSSL-based HMAC algorithm wrappers. class Abstract class << self + def aead(*v) + @aead = false if !defined?(@aead) + if v.empty? + @aead = superclass.aead if @aead.nil? && superclass.respond_to?(:aead) + return @aead + elsif v.length == 1 + @aead = v.first + else + raise ArgumentError, "wrong number of arguments (#{v.length} for 1)" + end + end + def etm(*v) @etm = false if !defined?(@etm) if v.empty? @@ -57,6 +69,10 @@ def digest_class(*v) end end + def aead + self.class.aead + end + def etm self.class.etm end diff --git a/lib/net/ssh/transport/packet_stream.rb b/lib/net/ssh/transport/packet_stream.rb index 3b4d85a0a..4aab7597d 100644 --- a/lib/net/ssh/transport/packet_stream.rb +++ b/lib/net/ssh/transport/packet_stream.rb @@ -128,7 +128,7 @@ def enqueue_packet(payload) # rubocop:disable Metrics/AbcSize payload = client.compress(payload) # the length of the packet, minus the padding - actual_length = (client.hmac.etm ? 0 : 4) + payload.bytesize + 1 + actual_length = (client.hmac.etm || client.hmac.aead ? 0 : 4) + payload.bytesize + 1 # compute the padding length padding_length = client.block_size - (actual_length % client.block_size) @@ -151,7 +151,7 @@ def enqueue_packet(payload) # rubocop:disable Metrics/AbcSize debug { "using encrypt-then-mac" } # Encrypt padding_length, payload, and padding. Take MAC - # from the unencrypted packet_lenght and the encrypted + # from the unencrypted packet_length and the encrypted # data. length_data = [packet_length].pack("N") @@ -219,7 +219,7 @@ def initialize_ssh # new Packet object. # rubocop:disable Metrics/AbcSize def poll_next_packet - aad_length = server.hmac.etm ? 4 : 0 + aad_length = server.hmac.etm || server.hmac.aead ? 4 : 0 if @packet.nil? minimum = server.block_size < 4 ? 4 : server.block_size diff --git a/lib/net/ssh/transport/state.rb b/lib/net/ssh/transport/state.rb index 1af6b869e..969f45901 100644 --- a/lib/net/ssh/transport/state.rb +++ b/lib/net/ssh/transport/state.rb @@ -125,7 +125,7 @@ def compress(data) compressor.deflate(data, Zlib::SYNC_FLUSH) end - # Deompresses the data. If no compression is in effect, this will just return + # Decompresses the data. If no compression is in effect, this will just return # the data unmodified, otherwise it uses #decompressor to decompress the data. def decompress(data) data = data.to_s diff --git a/test/integration/README.md b/test/integration/README.md index db8ec5db9..8454fc8b0 100644 --- a/test/integration/README.md +++ b/test/integration/README.md @@ -10,7 +10,7 @@ Setup: ansible-galaxy install rvm.ruby vagrant up ; vagrant ssh rvmsudo_secure_path=1 rvmsudo rvm all do gem install bundler - rvm all do sh -c 'rm Gemfile.lock; bundle' + rvmsudo_secure_path=1 rvmsudo rvm all do sh -c 'rm Gemfile.lock; bundle' rvm all do rake test # Debugging diff --git a/test/integration/playbook.yml b/test/integration/playbook.yml index 3a04060ab..69a2f61b0 100644 --- a/test/integration/playbook.yml +++ b/test/integration/playbook.yml @@ -30,6 +30,8 @@ register: openssl_version_query - name: Install openssl-1.1.1g block: + - name: "Install make gcc" + command: sh -c "sudo apt-get update && sudo apt install -y make gcc" - name: "Download openssl-1.1.1g sources" unarchive: src: https://www.openssl.org/source/openssl-1.1.1g.tar.gz diff --git a/test/integration/test_gcm_cipher.rb b/test/integration/test_gcm_cipher.rb new file mode 100644 index 000000000..7bc660092 --- /dev/null +++ b/test/integration/test_gcm_cipher.rb @@ -0,0 +1,53 @@ +require_relative 'common' +require 'fileutils' +require 'tmpdir' + +require 'net/ssh' + +require 'timeout' + +# bundle exec ruby -Ilib:test ./test/integration/test_gcm_cipher.rb + +# 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 TestGcmCipher < NetSSHTest + include IntegrationTestHelpers + + def run_with_only_cipher(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 #{cipher}") + + 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: cipher, password: 'foopwd', port: port, user_known_hosts_file: [f.path], verbose: :debug) do |ssh| + ssh.exec! "echo 'foo'" + end + assert_equal "foo\n", ret + rescue SocketError, Errno::ECONNREFUSED, Errno::EHOSTUNREACH + sleep 0.25 + retry + end + end + end + end + + def test_aes128_gcm + run_with_only_cipher('aes128-gcm@openssh.com') + end + + def test_aes256_gcm + run_with_only_cipher('aes256-gcm@openssh.com') + end +end diff --git a/test/transport/test_algorithms.rb b/test/transport/test_algorithms.rb index 7acd5c6a7..30c689b41 100644 --- a/test/transport/test_algorithms.rb +++ b/test/transport/test_algorithms.rb @@ -19,7 +19,7 @@ def test_allowed_packets def test_constructor_should_build_default_list_of_preferred_algorithms assert_equal ed_ec_host_keys + %w[ssh-rsa-cert-v01@openssh.com ssh-rsa-cert-v00@openssh.com ssh-rsa rsa-sha2-256 rsa-sha2-512], algorithms[:host_key] assert_equal x25519_kex + ec_kex + %w[diffie-hellman-group-exchange-sha256 diffie-hellman-group14-sha256 diffie-hellman-group14-sha1], algorithms[:kex] - assert_equal chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr], algorithms[:encryption] + assert_equal chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes256-gcm@openssh.com aes128-gcm@openssh.com], algorithms[:encryption] assert_equal %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com hmac-sha2-512 hmac-sha2-256 hmac-sha1], algorithms[:hmac] assert_equal %w[none zlib@openssh.com zlib], algorithms[:compression] assert_equal %w[], algorithms[:language] @@ -28,7 +28,7 @@ def test_constructor_should_build_default_list_of_preferred_algorithms def test_constructor_should_build_complete_list_of_algorithms_with_append_all_supported_algorithms assert_equal ed_ec_host_keys + %w[ssh-rsa-cert-v01@openssh.com ssh-rsa-cert-v00@openssh.com ssh-rsa rsa-sha2-256 rsa-sha2-512 ssh-dss], algorithms(append_all_supported_algorithms: true)[:host_key] assert_equal x25519_kex + ec_kex + %w[diffie-hellman-group-exchange-sha256 diffie-hellman-group14-sha256 diffie-hellman-group14-sha1 diffie-hellman-group-exchange-sha1 diffie-hellman-group1-sha1], algorithms(append_all_supported_algorithms: true)[:kex] - assert_equal chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes256-cbc aes192-cbc aes128-cbc rijndael-cbc@lysator.liu.se blowfish-ctr blowfish-cbc cast128-ctr cast128-cbc 3des-ctr 3des-cbc idea-cbc none], algorithms(append_all_supported_algorithms: true)[:encryption] + assert_equal chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes256-gcm@openssh.com aes128-gcm@openssh.com aes256-cbc aes192-cbc aes128-cbc rijndael-cbc@lysator.liu.se blowfish-ctr blowfish-cbc cast128-ctr cast128-cbc 3des-ctr 3des-cbc idea-cbc none], algorithms(append_all_supported_algorithms: true)[:encryption] assert_equal %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com hmac-sha2-512 hmac-sha2-256 hmac-sha1 hmac-sha2-512-96 hmac-sha2-256-96 hmac-sha1-96 hmac-ripemd160 hmac-ripemd160@openssh.com hmac-md5 hmac-md5-96 none], algorithms(append_all_supported_algorithms: true)[:hmac] assert_equal %w[none zlib@openssh.com zlib], algorithms(append_all_supported_algorithms: true)[:compression] assert_equal %w[], algorithms[:language] @@ -128,27 +128,27 @@ def test_constructor_with_preferred_kex_supports_removals_with_wildcard end def test_constructor_with_preferred_encryption_should_put_preferred_encryption_first - assert_equal %w[aes256-cbc] + chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes192-cbc aes128-cbc rijndael-cbc@lysator.liu.se blowfish-ctr blowfish-cbc cast128-ctr cast128-cbc 3des-ctr 3des-cbc idea-cbc none], algorithms(encryption: "aes256-cbc", append_all_supported_algorithms: true)[:encryption] + assert_equal %w[aes256-cbc] + chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes256-gcm@openssh.com aes128-gcm@openssh.com aes192-cbc aes128-cbc rijndael-cbc@lysator.liu.se blowfish-ctr blowfish-cbc cast128-ctr cast128-cbc 3des-ctr 3des-cbc idea-cbc none], algorithms(encryption: "aes256-cbc", append_all_supported_algorithms: true)[:encryption] end def test_constructor_with_multiple_preferred_encryption_should_put_all_preferred_encryption_first - assert_equal %w[aes256-cbc 3des-cbc idea-cbc] + chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes192-cbc aes128-cbc rijndael-cbc@lysator.liu.se blowfish-ctr blowfish-cbc cast128-ctr cast128-cbc 3des-ctr none], algorithms(encryption: %w[aes256-cbc 3des-cbc idea-cbc], append_all_supported_algorithms: true)[:encryption] + assert_equal %w[aes256-cbc 3des-cbc idea-cbc] + chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes256-gcm@openssh.com aes128-gcm@openssh.com aes192-cbc aes128-cbc rijndael-cbc@lysator.liu.se blowfish-ctr blowfish-cbc cast128-ctr cast128-cbc 3des-ctr none], algorithms(encryption: %w[aes256-cbc 3des-cbc idea-cbc], append_all_supported_algorithms: true)[:encryption] end def test_constructor_with_unrecognized_encryption_should_keep_whats_supported - assert_equal %w[aes256-cbc] + chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes192-cbc aes128-cbc rijndael-cbc@lysator.liu.se blowfish-ctr blowfish-cbc cast128-ctr cast128-cbc 3des-ctr 3des-cbc idea-cbc none], algorithms(encryption: %w[bogus aes256-cbc], append_all_supported_algorithms: true)[:encryption] + assert_equal %w[aes256-cbc] + chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes256-gcm@openssh.com aes128-gcm@openssh.com aes192-cbc aes128-cbc rijndael-cbc@lysator.liu.se blowfish-ctr blowfish-cbc cast128-ctr cast128-cbc 3des-ctr 3des-cbc idea-cbc none], algorithms(encryption: %w[bogus aes256-cbc], append_all_supported_algorithms: true)[:encryption] end def test_constructor_with_preferred_encryption_supports_additions # There's nothing we can really append to the set since the default algos # are frozen so this is really just testing that it doesn't do anything # unexpected. - assert_equal chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes256-cbc aes192-cbc aes128-cbc rijndael-cbc@lysator.liu.se blowfish-ctr blowfish-cbc cast128-ctr cast128-cbc 3des-ctr 3des-cbc idea-cbc none], + assert_equal chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes256-gcm@openssh.com aes128-gcm@openssh.com aes256-cbc aes192-cbc aes128-cbc rijndael-cbc@lysator.liu.se blowfish-ctr blowfish-cbc cast128-ctr cast128-cbc 3des-ctr 3des-cbc idea-cbc none], algorithms(encryption: %w[+3des-cbc])[:encryption] end def test_constructor_with_preferred_encryption_supports_removals_with_wildcard - assert_equal chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr cast128-ctr], + assert_equal chacha_poly_cipher + %w[aes256-ctr aes192-ctr aes128-ctr aes256-gcm@openssh.com aes128-gcm@openssh.com cast128-ctr], algorithms(encryption: %w[-rijndael-cbc@lysator.liu.se -blowfish-* -3des-* -*-cbc -none])[:encryption] end @@ -432,8 +432,8 @@ def assert_kexinit(buffer, options = {}) assert_equal 16, buffer.read(16).length assert_equal options[:kex] || (x25519_kex + ec_kex + %w[diffie-hellman-group-exchange-sha256 diffie-hellman-group14-sha256 diffie-hellman-group14-sha1]).join(','), buffer.read_string assert_equal options[:host_key] || (ed_ec_host_keys + %w[ssh-rsa-cert-v01@openssh.com ssh-rsa-cert-v00@openssh.com ssh-rsa rsa-sha2-256 rsa-sha2-512]).join(','), buffer.read_string - assert_equal options[:encryption_client] || "#{chacha_poly_cipher_str}aes256-ctr,aes192-ctr,aes128-ctr", buffer.read_string - assert_equal options[:encryption_server] || "#{chacha_poly_cipher_str}aes256-ctr,aes192-ctr,aes128-ctr", buffer.read_string + assert_equal options[:encryption_client] || "#{chacha_poly_cipher_str}aes256-ctr,aes192-ctr,aes128-ctr,aes256-gcm@openssh.com,aes128-gcm@openssh.com", buffer.read_string + assert_equal options[:encryption_server] || "#{chacha_poly_cipher_str}aes256-ctr,aes192-ctr,aes128-ctr,aes256-gcm@openssh.com,aes128-gcm@openssh.com", buffer.read_string assert_equal options[:hmac_client] || 'hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-sha1', buffer.read_string assert_equal options[:hmac_server] || 'hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256,hmac-sha1', buffer.read_string assert_equal options[:compression_client] || 'none,zlib@openssh.com,zlib', buffer.read_string diff --git a/test/transport/test_cipher_factory.rb b/test/transport/test_cipher_factory.rb index fc11008ad..69a2c3490 100644 --- a/test/transport/test_cipher_factory.rb +++ b/test/transport/test_cipher_factory.rb @@ -246,8 +246,21 @@ def test_none_for_decryption assert_equal TEXT, decrypt("none", TEXT) end + AES128_GCM = ["00000040b9a5166a6b4382e1989b55da47618b097b3b8cfdaa7f6d5b483e57ae60d542acb0525bd5fdee2bf127f8ae8293934b8da69f2afac6005818490df2ab87c24bcdeb3cc42d4ff26900bb97b1ac471067bb00000019a49b2768fb31ca36032e0431b342546144e12127f2fa142638a0a7f85c338576b2f47306da2fef6785"].pack('H*') + AES256_GCM = ["000000408a29a280b1d60b55d772d822ac890b565b96592d0eca0f0e70d530a17a91b74802577aab7ebabbd877dc86216f4ec8bd7b6220b139032f884bce1346164b7ab06718d6e08be7064609e771dfc50c25800000001984d432a5699ad5f9c6c6588101d0e5af507a522732077c9f9db85532741102669b63b63026a85e2a54"].pack('H*') + CHACHA20POLY1305 = ["73cbe4dd0d6495d2048bb1ba5f01f055f6271efaa5e56b9de3586bd116ededab481dc7833d5b6aaa7f7827d49c82185a02f62262d9efae8e6973a4e98251dcbbf2beebfc29c40a75192604729b6f7d412add8fe1a730e4b21c8aa1c73786090bd46bb66121c888b3e628c3f5a9a6a0738f8fcc2a611ef97bd0a665eb565ba0247d"].pack('H*') + OPTIONS_AES128_GCM = { + key: ["f4089170b3ae562c23cfcaebd73d3052"].pack("H*"), + aead: true + } + + OPTIONS_AES256_GCM = { + key: ["f4089170b3ae562c23cfcaebd73d30521b6812c0fd7e93346c8144d4de04e17b"].pack("H*"), + aead: true + } + OPTIONS_CHACHAPOLY = { iv: "ABC", key: "abcd" * 16, @@ -256,16 +269,18 @@ def test_none_for_decryption hash: '!@#$%#$^%$&^&%#$@$' } - def encrypt_cha_cha_poly(type) - cipher = factory.get(type, OPTIONS_CHACHAPOLY.merge(encrypt: true)) + def encrypt_implicit(type, options) + cipher = factory.get(type, options.merge(encrypt: true)) + cipher.nonce = ["000000000000000000000032"].pack('H*') if options[:aead] sequence_number = 1 result = cipher.update_cipher_mac(TEXT.dup[0...64], sequence_number) result << cipher.update_cipher_mac(TEXT.dup[64...], sequence_number + 1) result end - def decrypt_chacha_poly(type, data) - cipher = factory.get(type, OPTIONS_CHACHAPOLY.merge(decrypt: true)) + def decrypt_implicit(type, data, options) + cipher = factory.get(type, options.merge(encrypt: false)) + cipher.nonce = ["000000000000000000000032"].pack('H*') if options[:aead] result = "" sequence_number = 1 pos = 0 @@ -285,14 +300,30 @@ def decrypt_chacha_poly(type, data) def test_chacha20_poly1305_for_encryption skip "TODO: chacha20-poly1305 not loaded" unless Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED - ret = encrypt_cha_cha_poly("chacha20-poly1305@openssh.com") + ret = encrypt_implicit("chacha20-poly1305@openssh.com", OPTIONS_CHACHAPOLY) assert_equal CHACHA20POLY1305, ret end def test_chacha20_poly1305_for_decryption skip "TODO: chacha20-poly1305 not loaded" unless Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED - assert_equal TEXT, decrypt_chacha_poly("chacha20-poly1305@openssh.com", CHACHA20POLY1305) + assert_equal TEXT, decrypt_implicit("chacha20-poly1305@openssh.com", CHACHA20POLY1305, OPTIONS_CHACHAPOLY) + end + + def test_aes128_gcm_for_encryption + assert_equal AES128_GCM, encrypt_implicit("aes128-gcm@openssh.com", OPTIONS_AES128_GCM) + end + + def test_aes128_gcm_to_decryption + assert_equal TEXT, decrypt_implicit("aes128-gcm@openssh.com", AES128_GCM, OPTIONS_AES128_GCM) + end + + def test_aes256_gcm_for_encryption + assert_equal AES256_GCM, encrypt_implicit("aes256-gcm@openssh.com", OPTIONS_AES256_GCM) + end + + def test_aes256_gcm_for_decryption + assert_equal TEXT, decrypt_implicit("aes256-gcm@openssh.com", AES256_GCM, OPTIONS_AES256_GCM) end private diff --git a/test/transport/test_packet_stream.rb b/test/transport/test_packet_stream.rb index 773a7b6db..628c8bb06 100644 --- a/test/transport/test_packet_stream.rb +++ b/test/transport/test_packet_stream.rb @@ -209,6 +209,18 @@ def test_enqueue_utf_8_packet_should_ensure_packet_length_is_in_bytes_and_multip :standard => ["5aa01d263f83e1451c7d981526aa8e03b3ec44857a5dde471d76ba92fd92c9a77911c43ca96a13e37a5b1a346508016793f4a57a"].pack('H*') } }, + 'aes128-gcm@openssh.com' => { + 'implicit' => { + false => ["00000020462f5dc27e3ba9da491bbfa70deb5183ffc808c9178e505374ed437b46eb2474c470d68dc015cc677c91794a0d5603a8"].pack('H*'), + :standard => ["000000204f53c0a01f5fc0decc35838d40cf702b33830fbb07961334235d0cf9001c211636ef2046feff51f3e1d5e7375308896d"].pack('H*') + } + }, + 'aes256-gcm@openssh.com' => { + 'implicit' => { + false => ["00000020dfd5571fd6a781e395dcf18552a2b93628bf6a2f3ce456bd08dd3a457094b967d47977a309126e7d02dfc8d5f91a5588"].pack('H*'), + :standard => ["00000020d6a9ca7db7c3e8e710f2cdaf1f86989ee4f46d5d2cfc15da5f6d75c73663bc056b6644958591f155d06816aa87adbaff"].pack('H*') + } + }, "3des-cbc" => { "hmac-md5" => { false => "\003\352\031\261k\243\200\204\301\203]!\a\306\217\201\a[^\304\317\322\264\265~\361\017\n\205\272, \000\032w\312\t\306\374\271\345p\215\224\373\363\v\261", @@ -1059,7 +1071,7 @@ def test_enqueue_utf_8_packet_should_ensure_packet_length_is_in_bytes_and_multip ciphers = Net::SSH::Transport::CipherFactory::SSH_TO_OSSL.keys + Net::SSH::Transport::CipherFactory::SSH_TO_CLASS.keys hmacs = Net::SSH::Transport::HMAC::MAP.keys + ["implicit"] - implicit_ciphers = ["chacha20-poly1305@openssh.com"] + implicit_ciphers = %w[chacha20-poly1305@openssh.com aes256-gcm@openssh.com aes128-gcm@openssh.com] ciphers.each do |cipher_name| unless Net::SSH::Transport::CipherFactory.supported?(cipher_name) && PACKETS.key?(cipher_name) @@ -1093,6 +1105,8 @@ def test_enqueue_utf_8_packet_should_ensure_packet_length_is_in_bytes_and_multip Net::SSH::Transport::HMAC.get(hmac_name, "{}|", opts) end + cipher.nonce = ["000000000000000000000031"].pack('H*') if hmac.respond_to?(:aead) && hmac.aead + stream.server.set cipher: cipher, hmac: hmac, compression: compress stream.stubs(:recv).returns(PACKETS[cipher_name][hmac_name][compress]) IO.stubs(:select).returns([[stream]]) @@ -1116,6 +1130,8 @@ def test_enqueue_utf_8_packet_should_ensure_packet_length_is_in_bytes_and_multip Net::SSH::Transport::HMAC.get(hmac_name, "{}|", opts) end + cipher.nonce = ["000000000000000000000031"].pack('H*') if hmac.respond_to?(:aead) && hmac.aead + srand(100) stream.client.set cipher: cipher, hmac: hmac, compression: compress stream.enqueue_packet(ssh_packet)