diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b079dc12..ab6db7dc9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,7 +1,9 @@ name: CI on: pull_request: - push: { branches: master } + push: + branches: + - master jobs: test: runs-on: ubuntu-22.04 @@ -42,8 +44,8 @@ jobs: gem install bundler ${{ (startsWith(matrix.ruby-version, '2.6.') || startsWith(matrix.ruby-version, '2.7.')) && '-v 2.4.22' || '' }} bundle config set path 'vendor/bundle' bundle config set --local path 'vendor/bundle' - bundle install --jobs 4 --retry 3 --path vendor/bundle - BUNDLE_GEMFILE=./Gemfile.noed25519 bundle install --jobs 4 --retry 3 --path vendor/bundle + bundle install --jobs 4 --retry 3 + BUNDLE_GEMFILE=./Gemfile.noed25519 bundle install --jobs 4 --retry 3 env: BUNDLE_PATH: vendor/bundle diff --git a/lib/net/ssh/authentication/ed25519.rb b/lib/net/ssh/authentication/ed25519.rb index 7f33d7b8b..3b84403b3 100644 --- a/lib/net/ssh/authentication/ed25519.rb +++ b/lib/net/ssh/authentication/ed25519.rb @@ -64,25 +64,48 @@ def self.read(datafull, password) len = buffer.read_long - keylen, blocksize, ivlen = CipherFactory.get_lengths(ciphername, iv_len: true) - raise ArgumentError.new("Private key len:#{len} is not a multiple of #{blocksize}") if - ((len < blocksize) || ((blocksize > 0) && (len % blocksize) != 0)) - - if kdfname == 'bcrypt' - salt = kdfopts.read_string - rounds = kdfopts.read_long - - raise "BCryptPbkdf is not implemented for jruby" if RUBY_PLATFORM == "java" - - key = BCryptPbkdf::key(password, salt, keylen + ivlen, rounds) - raise DecryptError.new("BCyryptPbkdf failed", encrypted_key: true) unless key + if ciphername == 'none' + cipher = Transport::IdentityCipher else - key = '\x00' * (keylen + ivlen) + cipher = OpenSSL::Cipher.new(CipherFactory::SSH_TO_OSSL[ciphername]) + keylen = cipher.key_len + ivlen = cipher.iv_len + blocksize = cipher.block_size + + raise ArgumentError.new("Private key len:#{len} is not a multiple of #{blocksize}") if + ((len < blocksize) || ((blocksize > 0) && (len % blocksize) != 0)) + + if kdfname == 'bcrypt' + salt = kdfopts.read_string + rounds = kdfopts.read_long + + raise "BCryptPbkdf is not implemented for jruby" if RUBY_PLATFORM == "java" + + key = BCryptPbkdf::key(password, salt, keylen + ivlen, rounds) + raise DecryptError.new("BCryptPbkdf failed", encrypted_key: true) unless key + else + key = '\x00' * (keylen + ivlen) + end + + cipher.decrypt + cipher.key = key[0...keylen] + cipher.iv = key[keylen...keylen + ivlen] + cipher.padding = 0 end - cipher = CipherFactory.get(ciphername, key: key[0...keylen], iv: key[keylen...keylen + ivlen], decrypt: true) + encrypted_data = buffer.remainder_as_buffer.to_s + + # TODO: test with chacha poly + decoded = if cipher.authenticated? + ciphertext = encrypted_data[0...-16] + auth_tag = encrypted_data[-16..] + cipher.auth_tag = auth_tag + cipher.auth_data = '' + cipher.update(ciphertext) + else + cipher.update(encrypted_data) + end - decoded = cipher.update(buffer.remainder_as_buffer.to_s) decoded << cipher.final decoded = Net::SSH::Buffer.new(decoded) diff --git a/lib/net/ssh/transport/chacha20_poly1305_cipher.rb b/lib/net/ssh/transport/chacha20_poly1305_cipher.rb index e5c2c32db..71c7ca244 100644 --- a/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +++ b/lib/net/ssh/transport/chacha20_poly1305_cipher.rb @@ -111,6 +111,10 @@ def self.block_size def self.key_length 64 end + + def self.iv_len + 12 + end end end end diff --git a/lib/net/ssh/transport/cipher_factory.rb b/lib/net/ssh/transport/cipher_factory.rb index 9fc52ea31..2e8d9847d 100644 --- a/lib/net/ssh/transport/cipher_factory.rb +++ b/lib/net/ssh/transport/cipher_factory.rb @@ -30,6 +30,10 @@ class CipherFactory "aes128-ctr" => ::OpenSSL::Cipher.ciphers.include?("aes-128-ctr") ? "aes-128-ctr" : "aes-128-ecb", 'cast128-ctr' => 'cast5-ecb', + 'aes128-gcm@openssh.com' => 'aes-128-gcm', + 'aes256-gcm@openssh.com' => 'aes-256-gcm', + 'chacha20-poly1305@openssh.com' => 'chacha20-poly1305', + 'none' => 'none' } @@ -100,7 +104,11 @@ def self.get(name, options = {}) # 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? + unless klass.nil? + result = [klass.key_length, klass.block_size] + result << klass.iv_len if options[:iv_len] + return result + end ossl_name = SSH_TO_OSSL[name] if ossl_name.nil? || ossl_name == "none" diff --git a/lib/net/ssh/transport/gcm_cipher.rb b/lib/net/ssh/transport/gcm_cipher.rb index 0af1eb027..cabab3643 100644 --- a/lib/net/ssh/transport/gcm_cipher.rb +++ b/lib/net/ssh/transport/gcm_cipher.rb @@ -116,7 +116,7 @@ def self.block_size # N_MIN minimum nonce (IV) length 12 octets # N_MAX maximum nonce (IV) length 12 octets # - def iv_len + def self.iv_len 12 end diff --git a/lib/net/ssh/transport/identity_cipher.rb b/lib/net/ssh/transport/identity_cipher.rb index 6bd9c8cf7..783e55524 100644 --- a/lib/net/ssh/transport/identity_cipher.rb +++ b/lib/net/ssh/transport/identity_cipher.rb @@ -58,6 +58,10 @@ def reset def implicit_mac? false end + + def authenticated? + false + end end end end diff --git a/test/authentication/test_open_ssh_private_key_loader.rb b/test/authentication/test_open_ssh_private_key_loader.rb new file mode 100644 index 000000000..873b7159e --- /dev/null +++ b/test/authentication/test_open_ssh_private_key_loader.rb @@ -0,0 +1,68 @@ +unless ENV['NET_SSH_NO_ED25519'] + + require_relative '../common' + require 'net/ssh/authentication/ed25519_loader' + require 'net/ssh/key_factory' + require 'base64' + + module Authentication + class TestOpenSSHPrivateKeyLoader < NetSSHTest + def setup + raise "No ED25519 set NET_SSH_NO_ED25519 to ignore this test" unless Net::SSH::Authentication::ED25519Loader::LOADED + end + + def test_aes_cbc_key + key = <<~PRIVATEKEY + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABAkbe7i7M + lqGCSgPgr+ohv1AAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIHl0AyaA/DuN/ZYd + 7gbSDA1uCALeRaz2j/72tKkJQ6mrAAAAkIDyuOAcJhooRGOI1Vo3EWCaVWgQI7yT+Up2D8 + AOZ/xcOlcdrnSoVAPjYbeNGePuhShnSvzt3/ffzk8OAMkmVIfOukzQ1xRlySeFASWzXpZR + gJ4xdOyaURZ2zGeie29WOWJfeNCf/sKrtE8GonVW85iLVBBM1tDga9ta2Dq872OGFS7/qi + VCs5bN8YByGKbuxA== + -----END OPENSSH PRIVATE KEY----- + PRIVATEKEY + pwd = 'test' + + privkey = Net::SSH::Authentication::ED25519::OpenSSHPrivateKeyLoader.read(key, pwd) + assert_kind_of(Net::SSH::Authentication::ED25519::PrivKey, privkey) + end + + def test_aes256_gcm_key + key = <<~PRIVATEKEY + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAAFmFlczI1Ni1nY21Ab3BlbnNzaC5jb20AAAAGYmNyeXB0AA + AAGAAAABCCj7iTVdMrx15cLyopqOpcAAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAA + IPDBX9jvz2qS9ZFU+F9x+vL+ef7rH8VPwCn6IJc4CaGhAAAAkEyIM5bCRB50LPxP9D4rtD + EdReGh+wsN6/84u4Efw1QjRm/O6LJzJEDl3LV8ntDMWBqBN0q6OJL3eqICWYjAjOdPHLkl + qmKYhrT+eWDqS/1KihyO46HkfKOWSM2fOQPcjqVsjSTJ3CV5oVkptuLB6ak8e/mmzplywv + MjSA8aFdrtUGDJhpT7z46xURMJarvDkqvJZbw4PkmYWlfNRu3dXAI= + -----END OPENSSH PRIVATE KEY----- + PRIVATEKEY + pwd = 'test' + + privkey = Net::SSH::Authentication::ED25519::OpenSSHPrivateKeyLoader.read(key, pwd) + assert_kind_of(Net::SSH::Authentication::ED25519::PrivKey, privkey) + end + + def test_chacha20_poly1305_key + key = <<~PRIVATEKEY + -----BEGIN OPENSSH PRIVATE KEY----- + b3BlbnNzaC1rZXktdjEAAAAAHWNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc2guY29tAAAABm + JjcnlwdAAAABgAAAAQgMsN42jlw2C+pMgTPx+suAAAABgAAAABAAAAMwAAAAtzc2gtZWQy + NTUxOQAAACCHThbU/SJU7ntvbok6ANB0ob4Q36gXQxUj40PDGJGw4AAAAJADmcQtG5SDxI + srhPwRMOUvwK3niQ6R/vxuHrAXiCt9oMymG2ALOmt08no/MVgxeQwKGGFgSzVjFaq6Nyzg + yWA5df/AxUK72z7cqUaGzyMWQ+N4pC1q5pOINIiDxtjUTgo2Nv3ZbNV8EBGeDYX95iTN5G + YHeAFEd6hZKLOSMUDcKdj1vkZClWTHZBNJtIg4a4ZlQ8/mSJCf7TBv9z1ibaOh + -----END OPENSSH PRIVATE KEY----- + PRIVATEKEY + pwd = 'test' + + privkey = Net::SSH::Authentication::ED25519::OpenSSHPrivateKeyLoader.read(key, pwd) + assert_kind_of(Net::SSH::Authentication::ED25519::PrivKey, privkey) + end + + def test_unencrypted_key; end + end + end +end diff --git a/test/integration/common.rb b/test/integration/common.rb index a21673f97..dd56528d9 100644 --- a/test/integration/common.rb +++ b/test/integration/common.rb @@ -24,9 +24,18 @@ def sshd_8_or_later? !!(`sshd -v 2>&1 |grep 'OpenSSH_'` =~ /OpenSSH_8./) end - def ssh_keygen(file, type = 'rsa', password = '') + def ssh_keygen(file, type = 'rsa', password = '', cipher = nil) sh "rm -rf #{file} #{file}.pub" - sh "ssh-keygen #{ssh_keygen_format} -q -f #{file} -t #{type} -N '#{password}'" + cmd_words = [ + 'ssh-keygen', + ssh_keygen_format, + '-q', + '-f', file, + '-t', type, + '-N', "'#{password}'" + ] + cmd_words += ['-Z', cipher] if cipher + sh cmd_words.join(' ') end def ssh_keygen_format diff --git a/test/integration/test_ed25519_pkeys.rb b/test/integration/test_ed25519_pkeys.rb index 0fee8b003..d96098b31 100644 --- a/test/integration/test_ed25519_pkeys.rb +++ b/test/integration/test_ed25519_pkeys.rb @@ -43,7 +43,22 @@ def test_ssh_agent def test_in_file_with_password Dir.mktmpdir do |dir| - ssh_keygen "#{dir}/id_rsa_ed25519", "ed25519" + ssh_keygen "#{dir}/id_rsa_ed25519", "ed25519", "pwd" + set_authorized_key('net_ssh_1', "#{dir}/id_rsa_ed25519.pub") + + # TODO: fix bug in net ssh which reads public key even if private key is there + sh "mv #{dir}/id_rsa_ed25519.pub #{dir}/id_rsa_ed25519.pub.hidden" + + ret = Net::SSH.start("localhost", "net_ssh_1", { keys: "#{dir}/id_rsa_ed25519", passphrase: 'pwd' }) do |ssh| + ssh.exec! 'echo "hello from:$USER"' + end + assert_equal "hello from:net_ssh_1\n", ret + end + end + + def test_in_file_with_password + Dir.mktmpdir do |dir| + ssh_keygen "#{dir}/id_rsa_ed25519", "ed25519", "pwd", "aes256-gcm@openssh.com" set_authorized_key('net_ssh_1', "#{dir}/id_rsa_ed25519.pub") # TODO: fix bug in net ssh which reads public key even if private key is there diff --git a/test/transport/test_cipher_factory.rb b/test/transport/test_cipher_factory.rb index 818118918..519ba896c 100644 --- a/test/transport/test_cipher_factory.rb +++ b/test/transport/test_cipher_factory.rb @@ -11,65 +11,98 @@ def self.if_supported?(name) def test_lengths_for_none assert_equal [0, 0], factory.get_lengths("none") + assert_equal [0, 0, 0], factory.get_lengths("none", iv_len: true) assert_equal [0, 0], factory.get_lengths("bogus") + assert_equal [0, 0, 0], factory.get_lengths("bogus", iv_len: true) end def test_lengths_for_blowfish_cbc assert_equal [16, 8], factory.get_lengths("blowfish-cbc") + assert_equal [16, 8, 8], factory.get_lengths("blowfish-cbc", iv_len: true) end if_supported?("idea-cbc") do def test_lengths_for_idea_cbc assert_equal [16, 8], factory.get_lengths("idea-cbc") + assert_equal [16, 8, 8], factory.get_lengths("idea-cbc", iv_len: true) end end def test_lengths_for_rijndael_cbc assert_equal [32, 16], factory.get_lengths("rijndael-cbc@lysator.liu.se") + assert_equal [32, 16, 16], factory.get_lengths("rijndael-cbc@lysator.liu.se", iv_len: true) end def test_lengths_for_cast128_cbc assert_equal [16, 8], factory.get_lengths("cast128-cbc") + assert_equal [16, 8, 8], factory.get_lengths("cast128-cbc", iv_len: true) end def test_lengths_for_3des_cbc assert_equal [24, 8], factory.get_lengths("3des-cbc") + assert_equal [24, 8, 8], factory.get_lengths("3des-cbc", iv_len: true) end def test_lengths_for_aes128_cbc assert_equal [16, 16], factory.get_lengths("aes128-cbc") + assert_equal [16, 16, 16], factory.get_lengths("aes128-cbc", iv_len: true) end def test_lengths_for_aes192_cbc assert_equal [24, 16], factory.get_lengths("aes192-cbc") + assert_equal [24, 16, 16], factory.get_lengths("aes192-cbc", iv_len: true) end def test_lengths_for_aes256_cbc assert_equal [32, 16], factory.get_lengths("aes256-cbc") + assert_equal [32, 16, 16], factory.get_lengths("aes256-cbc", iv_len: true) end def test_lengths_for_3des_ctr assert_equal [24, 8], factory.get_lengths("3des-ctr") + assert_equal [24, 8, 0], factory.get_lengths("3des-ctr", iv_len: true) end def test_lengths_for_aes128_ctr assert_equal [16, 16], factory.get_lengths("aes128-ctr") + assert_equal [16, 16, 16], factory.get_lengths("aes128-ctr", iv_len: true) end def test_lengths_for_aes192_ctr assert_equal [24, 16], factory.get_lengths("aes192-ctr") + assert_equal [24, 16, 16], factory.get_lengths("aes192-ctr", iv_len: true) end def test_lengths_for_aes256_ctr assert_equal [32, 16], factory.get_lengths("aes256-ctr") + assert_equal [32, 16, 16], factory.get_lengths("aes256-ctr", iv_len: true) end def test_lengths_for_blowfish_ctr assert_equal [16, 8], factory.get_lengths("blowfish-ctr") + assert_equal [16, 8, 0], factory.get_lengths("blowfish-ctr", iv_len: true) end def test_lengths_for_cast128_ctr assert_equal [16, 8], factory.get_lengths("cast128-ctr") + assert_equal [16, 8, 0], factory.get_lengths("cast128-ctr", iv_len: true) + end + + def test_lengths_for_aes128_gcm + assert_equal [16, 16], factory.get_lengths("aes128-gcm@openssh.com") + assert_equal [16, 16, 12], factory.get_lengths("aes128-gcm@openssh.com", iv_len: true) + end + + def test_lengths_for_aes256_gcm + assert_equal [32, 16], factory.get_lengths("aes256-gcm@openssh.com") + assert_equal [32, 16, 12], factory.get_lengths("aes256-gcm@openssh.com", iv_len: true) + end + + def test_lengths_for_chacha20_poly1305 + skip "chacha20-poly1305 not loaded" unless Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED + + assert_equal [16, 64], factory.get_lengths("chacha20-poly1305@openssh.com") + assert_equal [16, 64, 12], factory.get_lengths("chacha20-poly1305@openssh.com", iv_len: true) end BLOWFISH_CBC = "\210\021\200\315\240_\026$\352\204g\233\244\242x\332e\370\001\327\224Nv@9_\323\037\252kb\037\036\237\375]\343/y\037\237\312Q\f7]\347Y\005\275%\377\0010$G\272\250B\265Nd\375\342\372\025r6}+Y\213y\n\237\267\\\374^\346BdJ$\353\220Ik\023<\236&H\277=\225"