Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
name: CI
on:
pull_request:
push: { branches: master }
push:
branches:
- master
jobs:
test:
runs-on: ubuntu-22.04
Expand Down Expand Up @@ -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

Expand Down
26 changes: 24 additions & 2 deletions lib/net/ssh/authentication/ed25519.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,31 @@ def self.read(datafull, password)
key = '\x00' * (keylen + ivlen)
end

cipher = CipherFactory.get(ciphername, key: key[0...keylen], iv: key[keylen...keylen + ivlen], decrypt: true)
if ciphername == 'none'
cipher = Transport::IdentityCipher
else
cipher = OpenSSL::Cipher.new(CipherFactory::SSH_TO_OSSL[ciphername])
cipher.decrypt
cipher.key = key[0...keylen]
cipher.iv = key[keylen...keylen + ivlen]
cipher.padding = 0
end
Comment on lines +67 to +94
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tested manually with AES-CBC, AES-GCM, and unencrypted keys. This doesn't yet work with ChaCha20-Poly1305; it looks like the corresponding class defines key_length to be 64, which is the bytes of key material required to maintain two ciphers.

While that may be necessary for transport, it's not for decrypting a private key. We may be better off interrogating the OpenSSL::Cipher#key_len (and iv_len) directly instead of using CipherFactory.get_lengths above.


encrypted_data = buffer.remainder_as_buffer.to_s

# TODO: test with chacha poly
decoded = if cipher.authenticated?
# tested with GCM
ciphertext = encrypted_data[0...-16]
auth_tag = encrypted_data[-16..]
cipher.auth_tag = auth_tag
cipher.auth_data = ''
cipher.update(ciphertext)
else
# tested with CBC
cipher.update(encrypted_data)
end

decoded = cipher.update(buffer.remainder_as_buffer.to_s)
decoded << cipher.final

decoded = Net::SSH::Buffer.new(decoded)
Expand Down
4 changes: 4 additions & 0 deletions lib/net/ssh/transport/chacha20_poly1305_cipher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ def self.block_size
def self.key_length
64
end

def self.iv_len
12
end
end
end
end
Expand Down
10 changes: 9 additions & 1 deletion lib/net/ssh/transport/cipher_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}

Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion lib/net/ssh/transport/gcm_cipher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions lib/net/ssh/transport/identity_cipher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ def reset
def implicit_mac?
false
end

def authenticated?
false
end
end
end
end
Expand Down
13 changes: 11 additions & 2 deletions test/integration/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 16 additions & 1 deletion test/integration/test_ed25519_pkeys.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions test/transport/test_cipher_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading