net-ssh 7.3.6 → 8.0.0.beta3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/.github/workflows/ci-with-docker.yml +0 -3
- data/.github/workflows/ci.yml +2 -17
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +1 -0
- data/CHANGES.txt +2 -15
- data/Dockerfile +1 -4
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +1 -1
- data/README.md +10 -13
- data/appveyor.yml +4 -16
- data/docker-compose.yml +1 -7
- data/lib/net/ssh/authentication/agent.rb +4 -4
- data/lib/net/ssh/authentication/ed25519.rb +134 -46
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/key_factory.rb +3 -4
- data/lib/net/ssh/proxy/command.rb +3 -28
- data/lib/net/ssh/proxy/jump.rb +3 -13
- data/lib/net/ssh/service/forward.rb +0 -6
- data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +89 -16
- data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +20 -1
- data/lib/net/ssh/transport/cipher_factory.rb +23 -1
- data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
- data/lib/net/ssh/transport/kex/curve25519_sha256.rb +41 -9
- data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +14 -6
- data/lib/net/ssh/transport/packet_stream.rb +0 -4
- data/lib/net/ssh/transport/server_version.rb +13 -33
- data/lib/net/ssh/transport/session.rb +1 -18
- data/lib/net/ssh/verifiers/always.rb +0 -3
- data/lib/net/ssh/version.rb +4 -4
- data/net-ssh.gemspec +3 -7
- data.tar.gz.sig +0 -0
- metadata +12 -42
- metadata.gz.sig +0 -0
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
|
@@ -16,11 +16,6 @@ module Net
|
|
|
16
16
|
# ...
|
|
17
17
|
# end
|
|
18
18
|
class Command
|
|
19
|
-
# The command line is run by the shell, so like OpenSSH 9.6 (CVE-2023-51385) we refuse
|
|
20
|
-
# host and user names containing shell metacharacters rather than substituting them.
|
|
21
|
-
INVALID_HOST_NAME = /\A-|[[:space:][:cntrl:]'`"$\\;&<>|(){},]/.freeze
|
|
22
|
-
INVALID_USER_NAME = /\A-|[[:cntrl:]'`";&<>|(){}]|[[:space:]]-|\\\z/.freeze
|
|
23
|
-
|
|
24
19
|
# The command line template
|
|
25
20
|
attr_reader :command_line_template
|
|
26
21
|
|
|
@@ -46,13 +41,13 @@ module Net
|
|
|
46
41
|
command_line = @command_line_template.gsub(/%(.)/) {
|
|
47
42
|
case $1
|
|
48
43
|
when 'h'
|
|
49
|
-
|
|
44
|
+
host
|
|
50
45
|
when 'p'
|
|
51
|
-
|
|
46
|
+
port.to_s
|
|
52
47
|
when 'r'
|
|
53
48
|
remote_user = connection_options && connection_options[:remote_user]
|
|
54
49
|
if remote_user
|
|
55
|
-
|
|
50
|
+
remote_user
|
|
56
51
|
else
|
|
57
52
|
raise ArgumentError, "remote user name not available"
|
|
58
53
|
end
|
|
@@ -122,26 +117,6 @@ module Net
|
|
|
122
117
|
Process.kill('TERM', io.pid)
|
|
123
118
|
Thread.new { io.close }
|
|
124
119
|
end
|
|
125
|
-
|
|
126
|
-
private
|
|
127
|
-
|
|
128
|
-
def validate_host_name!(host)
|
|
129
|
-
raise ArgumentError, "host name contains invalid characters: #{host.inspect}" if host.to_s.match?(INVALID_HOST_NAME)
|
|
130
|
-
|
|
131
|
-
host
|
|
132
|
-
end
|
|
133
|
-
|
|
134
|
-
def validate_user_name!(user)
|
|
135
|
-
raise ArgumentError, "user name contains invalid characters: #{user.inspect}" if user.to_s.match?(INVALID_USER_NAME)
|
|
136
|
-
|
|
137
|
-
user
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
def validate_port!(port)
|
|
141
|
-
raise ArgumentError, "port is not a number: #{port.inspect}" unless port.to_s.match?(/\A\d+\z/)
|
|
142
|
-
|
|
143
|
-
port.to_s
|
|
144
|
-
end
|
|
145
120
|
end
|
|
146
121
|
end
|
|
147
122
|
end
|
data/lib/net/ssh/proxy/jump.rb
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
require 'shellwords'
|
|
2
1
|
require 'uri'
|
|
3
2
|
require 'net/ssh/proxy/command'
|
|
4
3
|
|
|
@@ -35,29 +34,20 @@ module Net
|
|
|
35
34
|
# We cannot build the ProxyCommand template until we know if the :config
|
|
36
35
|
# option was specified during `Net::SSH.start`.
|
|
37
36
|
def build_proxy_command_equivalent(connection_options = nil)
|
|
38
|
-
extra_jumps = jump_proxies.split(",", 2)
|
|
37
|
+
first_jump, extra_jumps = jump_proxies.split(",", 2)
|
|
39
38
|
config = connection_options && connection_options[:config]
|
|
40
|
-
uri
|
|
39
|
+
uri = URI.parse("ssh://#{first_jump}")
|
|
41
40
|
|
|
42
41
|
template = "ssh".dup
|
|
43
42
|
template << " -l #{uri.user}" if uri.user
|
|
44
43
|
template << " -p #{uri.port}" if uri.port
|
|
45
44
|
template << " -J #{extra_jumps}" if extra_jumps
|
|
46
|
-
template << " -F #{
|
|
45
|
+
template << " -F #{config}" if config != true && config
|
|
47
46
|
template << " -W %h:%p "
|
|
48
47
|
template << uri.host
|
|
49
48
|
|
|
50
49
|
@command_line_template = template
|
|
51
50
|
end
|
|
52
|
-
|
|
53
|
-
private
|
|
54
|
-
|
|
55
|
-
def validate_jump!(jump)
|
|
56
|
-
uri = URI.parse("ssh://#{jump}")
|
|
57
|
-
validate_user_name!(uri.user) if uri.user
|
|
58
|
-
validate_host_name!(uri.host)
|
|
59
|
-
uri
|
|
60
|
-
end
|
|
61
51
|
end
|
|
62
52
|
end
|
|
63
53
|
end
|
|
@@ -405,12 +405,6 @@ module Net
|
|
|
405
405
|
|
|
406
406
|
# The callback used when an auth-agent channel is requested by the server.
|
|
407
407
|
def auth_agent_channel(session, channel, packet)
|
|
408
|
-
# Like OpenSSH, only relay the local agent when forwarding was requested (GHSA-qpg5-fx6p-8cq3).
|
|
409
|
-
unless session.options[:forward_agent] || @agent_forwarded
|
|
410
|
-
error { "server tried agent forwarding, which was not requested; refusing" }
|
|
411
|
-
raise Net::SSH::ChannelOpenFailed.new(1, "agent forwarding was not requested")
|
|
412
|
-
end
|
|
413
|
-
|
|
414
408
|
info { "opening auth-agent channel" }
|
|
415
409
|
channel[:invisible] = true
|
|
416
410
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
require '
|
|
1
|
+
require 'openssl'
|
|
2
2
|
require 'net/ssh/loggable'
|
|
3
3
|
|
|
4
4
|
module Net
|
|
@@ -8,6 +8,15 @@ module Net
|
|
|
8
8
|
class ChaCha20Poly1305Cipher
|
|
9
9
|
include Net::SSH::Loggable
|
|
10
10
|
|
|
11
|
+
POLY1305_ALGORITHM = "POLY1305"
|
|
12
|
+
POLY1305_KEY_BYTES = 32
|
|
13
|
+
POLY1305_TAG_BYTES = 16
|
|
14
|
+
NAME = "chacha20-poly1305@openssh.com"
|
|
15
|
+
ZERO_BLOCK = "\x00".b * POLY1305_KEY_BYTES
|
|
16
|
+
ZERO_IV = "\x00".b * 16
|
|
17
|
+
|
|
18
|
+
class UnsupportedError < StandardError; end
|
|
19
|
+
|
|
11
20
|
# Implicit HMAC, no need to do anything
|
|
12
21
|
class ImplicitHMac
|
|
13
22
|
def etm
|
|
@@ -24,7 +33,6 @@ module Net
|
|
|
24
33
|
@chacha_hdr = OpenSSL::Cipher.new("chacha20")
|
|
25
34
|
key_len = @chacha_hdr.key_len
|
|
26
35
|
@chacha_main = OpenSSL::Cipher.new("chacha20")
|
|
27
|
-
@poly = RbNaCl::OneTimeAuths::Poly1305
|
|
28
36
|
if key.size < key_len * 2
|
|
29
37
|
error { "chacha20_poly1305: keylength doesn't match" }
|
|
30
38
|
raise "chacha20_poly1305: keylength doesn't match"
|
|
@@ -43,9 +51,9 @@ module Net
|
|
|
43
51
|
end
|
|
44
52
|
|
|
45
53
|
def update_cipher_mac(payload, sequence_number)
|
|
46
|
-
iv_data =
|
|
54
|
+
iv_data = packet_iv(sequence_number)
|
|
47
55
|
@chacha_main.iv = iv_data
|
|
48
|
-
poly_key = @chacha_main.update(
|
|
56
|
+
poly_key = @chacha_main.update(ZERO_BLOCK)
|
|
49
57
|
|
|
50
58
|
packet_length = payload.size
|
|
51
59
|
length_data = [packet_length].pack("N")
|
|
@@ -57,35 +65,36 @@ module Net
|
|
|
57
65
|
unencrypted_data = payload
|
|
58
66
|
packet += @chacha_main.update(unencrypted_data)
|
|
59
67
|
|
|
60
|
-
packet +=
|
|
68
|
+
packet += self.class.poly1305_auth(poly_key, packet)
|
|
61
69
|
return packet
|
|
62
70
|
end
|
|
63
71
|
|
|
64
72
|
def read_length(data, sequence_number)
|
|
65
|
-
iv_data =
|
|
73
|
+
iv_data = packet_iv(sequence_number)
|
|
66
74
|
@chacha_hdr.iv = iv_data
|
|
67
75
|
@chacha_hdr.update(data).unpack1("N")
|
|
68
76
|
end
|
|
69
77
|
|
|
70
78
|
def read_and_mac(data, mac, sequence_number)
|
|
71
|
-
iv_data =
|
|
79
|
+
iv_data = packet_iv(sequence_number)
|
|
72
80
|
@chacha_main.iv = iv_data
|
|
73
|
-
poly_key = @chacha_main.update(
|
|
81
|
+
poly_key = @chacha_main.update(ZERO_BLOCK)
|
|
74
82
|
|
|
75
83
|
iv_data[0] = 1.chr
|
|
76
84
|
@chacha_main.iv = iv_data
|
|
77
85
|
unencrypted_data = @chacha_main.update(data[4..])
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
|
|
87
|
+
expected_mac = self.class.poly1305_auth(poly_key, data[0..])
|
|
88
|
+
valid_mac = mac.respond_to?(:bytesize) &&
|
|
89
|
+
mac.bytesize == POLY1305_TAG_BYTES &&
|
|
90
|
+
OpenSSL.fixed_length_secure_compare(expected_mac, mac)
|
|
91
|
+
raise Net::SSH::Exception, "corrupted hmac detected #{name}" unless valid_mac
|
|
92
|
+
|
|
84
93
|
return unencrypted_data
|
|
85
94
|
end
|
|
86
95
|
|
|
87
96
|
def mac_length
|
|
88
|
-
|
|
97
|
+
POLY1305_TAG_BYTES
|
|
89
98
|
end
|
|
90
99
|
|
|
91
100
|
def block_size
|
|
@@ -93,7 +102,7 @@ module Net
|
|
|
93
102
|
end
|
|
94
103
|
|
|
95
104
|
def name
|
|
96
|
-
|
|
105
|
+
NAME
|
|
97
106
|
end
|
|
98
107
|
|
|
99
108
|
def implicit_mac?
|
|
@@ -111,6 +120,70 @@ module Net
|
|
|
111
120
|
def self.key_length
|
|
112
121
|
64
|
|
113
122
|
end
|
|
123
|
+
|
|
124
|
+
def self.iv_len
|
|
125
|
+
0
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def self.auth_length
|
|
129
|
+
POLY1305_TAG_BYTES
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def self.decrypt_private_key(ciphertext, auth_tag, key, _initialization_vector)
|
|
133
|
+
raise ArgumentError, "chacha20_poly1305: keylength doesn't match" unless
|
|
134
|
+
key.respond_to?(:bytesize) && key.bytesize == key_length
|
|
135
|
+
|
|
136
|
+
ciphertext = binary_string(ciphertext)
|
|
137
|
+
chacha = OpenSSL::Cipher.new("chacha20")
|
|
138
|
+
chacha.decrypt
|
|
139
|
+
chacha.key = binary_string(key[0...POLY1305_KEY_BYTES])
|
|
140
|
+
|
|
141
|
+
iv_data = ZERO_IV.dup
|
|
142
|
+
chacha.iv = iv_data
|
|
143
|
+
poly_key = chacha.update(ZERO_BLOCK)
|
|
144
|
+
|
|
145
|
+
valid_mac = OpenSSL.fixed_length_secure_compare(poly1305_auth(poly_key, ciphertext), auth_tag)
|
|
146
|
+
raise Net::SSH::Exception, "corrupted hmac detected #{NAME}" unless valid_mac
|
|
147
|
+
|
|
148
|
+
iv_data.setbyte(0, 1)
|
|
149
|
+
chacha.iv = iv_data
|
|
150
|
+
chacha.update(ciphertext)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def self.ensure_supported!
|
|
154
|
+
raise UnsupportedError, "OpenSSL::PKey raw private key APIs are unavailable" unless OpenSSL::PKey.respond_to?(:new_raw_private_key)
|
|
155
|
+
|
|
156
|
+
OpenSSL::Cipher.new("chacha20")
|
|
157
|
+
|
|
158
|
+
tag = poly1305_auth("\x00" * POLY1305_KEY_BYTES, "")
|
|
159
|
+
raise UnsupportedError, "OpenSSL Poly1305 authentication failed" unless tag.bytesize == POLY1305_TAG_BYTES
|
|
160
|
+
rescue OpenSSL::Cipher::CipherError, OpenSSL::PKey::PKeyError => e
|
|
161
|
+
raise UnsupportedError, e.message
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.poly1305_auth(poly_key, data)
|
|
165
|
+
validate_poly1305_key!(poly_key)
|
|
166
|
+
OpenSSL::PKey.new_raw_private_key(POLY1305_ALGORITHM, binary_string(poly_key)).sign(nil, binary_string(data))
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def self.validate_poly1305_key!(poly_key)
|
|
170
|
+
raise ArgumentError, "invalid Poly1305 key" unless poly_key.respond_to?(:bytesize) && poly_key.bytesize == POLY1305_KEY_BYTES
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.binary_string(string)
|
|
174
|
+
string.dup.force_encoding('BINARY')
|
|
175
|
+
end
|
|
176
|
+
private_class_method :binary_string
|
|
177
|
+
|
|
178
|
+
def packet_iv(sequence_number)
|
|
179
|
+
iv_data = ZERO_IV.dup
|
|
180
|
+
iv_data.setbyte(12, (sequence_number >> 24) & 0xff)
|
|
181
|
+
iv_data.setbyte(13, (sequence_number >> 16) & 0xff)
|
|
182
|
+
iv_data.setbyte(14, (sequence_number >> 8) & 0xff)
|
|
183
|
+
iv_data.setbyte(15, sequence_number & 0xff)
|
|
184
|
+
iv_data
|
|
185
|
+
end
|
|
186
|
+
private :packet_iv
|
|
114
187
|
end
|
|
115
188
|
end
|
|
116
189
|
end
|
|
@@ -1,15 +1,34 @@
|
|
|
1
1
|
module Net
|
|
2
2
|
module SSH
|
|
3
3
|
module Transport
|
|
4
|
-
# Loads chacha20
|
|
4
|
+
# Loads chacha20-poly1305 support backed by Ruby OpenSSL APIs.
|
|
5
5
|
module ChaCha20Poly1305CipherLoader
|
|
6
6
|
begin
|
|
7
7
|
require 'net/ssh/transport/chacha20_poly1305_cipher'
|
|
8
|
+
Net::SSH::Transport::ChaCha20Poly1305Cipher.ensure_supported!
|
|
8
9
|
LOADED = true
|
|
9
10
|
ERROR = nil
|
|
10
11
|
rescue LoadError => e
|
|
11
12
|
ERROR = e
|
|
12
13
|
LOADED = false
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
if defined?(Net::SSH::Transport::ChaCha20Poly1305Cipher::UnsupportedError) &&
|
|
16
|
+
e.is_a?(Net::SSH::Transport::ChaCha20Poly1305Cipher::UnsupportedError)
|
|
17
|
+
ERROR = e
|
|
18
|
+
LOADED = false
|
|
19
|
+
else
|
|
20
|
+
raise
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.raiseUnlessLoaded(message)
|
|
25
|
+
description = LOADED ? '' : chacha20_poly1305_support_required
|
|
26
|
+
description += "#{ERROR.class} : \"#{ERROR.message}\"\n" if ERROR
|
|
27
|
+
raise NotImplementedError, "#{message}\n#{description}" unless LOADED
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.chacha20_poly1305_support_required
|
|
31
|
+
"net-ssh requires openssl >= 3.2.0 and a crypto backend with ChaCha20 and Poly1305 enabled for chacha20-poly1305@openssh.com support.\n"
|
|
13
32
|
end
|
|
14
33
|
end
|
|
15
34
|
end
|
|
@@ -100,7 +100,11 @@ module Net
|
|
|
100
100
|
# if :iv_len option is supplied the third return value will be ivlen
|
|
101
101
|
def self.get_lengths(name, options = {})
|
|
102
102
|
klass = SSH_TO_CLASS[name]
|
|
103
|
-
|
|
103
|
+
unless klass.nil?
|
|
104
|
+
result = [klass.key_length, klass.block_size]
|
|
105
|
+
result << klass.iv_len if options[:iv_len]
|
|
106
|
+
return result
|
|
107
|
+
end
|
|
104
108
|
|
|
105
109
|
ossl_name = SSH_TO_OSSL[name]
|
|
106
110
|
if ossl_name.nil? || ossl_name == "none"
|
|
@@ -124,6 +128,24 @@ module Net
|
|
|
124
128
|
end
|
|
125
129
|
result
|
|
126
130
|
end
|
|
131
|
+
|
|
132
|
+
def self.auth_length(name)
|
|
133
|
+
klass = SSH_TO_CLASS[name]
|
|
134
|
+
return klass.auth_length if klass.respond_to?(:auth_length)
|
|
135
|
+
|
|
136
|
+
0
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.decrypt_private_key(name, ciphertext, auth_tag, key, initialization_vector)
|
|
140
|
+
klass = SSH_TO_CLASS[name]
|
|
141
|
+
raise Net::SSH::Exception, "#{name} does not support private key decryption" unless
|
|
142
|
+
klass.respond_to?(:decrypt_private_key)
|
|
143
|
+
|
|
144
|
+
raise Net::SSH::Exception, "incorrect auth_tag length" unless
|
|
145
|
+
auth_tag.respond_to?(:bytesize) && auth_tag.bytesize == klass.auth_length
|
|
146
|
+
|
|
147
|
+
klass.decrypt_private_key(ciphertext, auth_tag, key, initialization_vector)
|
|
148
|
+
end
|
|
127
149
|
end
|
|
128
150
|
end
|
|
129
151
|
end
|
|
@@ -5,7 +5,11 @@ module Net
|
|
|
5
5
|
module Transport
|
|
6
6
|
## Extension module for aes(128|256)gcm ciphers
|
|
7
7
|
module GCMCipher
|
|
8
|
-
|
|
8
|
+
BLOCK_SIZE = 16
|
|
9
|
+
IV_BYTES = 12
|
|
10
|
+
AUTH_TAG_BYTES = 16
|
|
11
|
+
|
|
12
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
9
13
|
def self.extended(orig)
|
|
10
14
|
# rubocop:disable Metrics/BlockLength
|
|
11
15
|
orig.class_eval do
|
|
@@ -100,15 +104,32 @@ module Net
|
|
|
100
104
|
end
|
|
101
105
|
|
|
102
106
|
def mac_length
|
|
103
|
-
|
|
107
|
+
GCMCipher::AUTH_TAG_BYTES
|
|
104
108
|
end
|
|
105
109
|
|
|
106
110
|
def block_size
|
|
107
|
-
|
|
111
|
+
GCMCipher::BLOCK_SIZE
|
|
108
112
|
end
|
|
109
113
|
|
|
110
114
|
def self.block_size
|
|
111
|
-
|
|
115
|
+
GCMCipher::BLOCK_SIZE
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.iv_len
|
|
119
|
+
GCMCipher::IV_BYTES
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.auth_length
|
|
123
|
+
GCMCipher::AUTH_TAG_BYTES
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.decrypt_private_key(ciphertext, auth_tag, key, initialization_vector)
|
|
127
|
+
cipher = new(encrypt: false, key: key)
|
|
128
|
+
|
|
129
|
+
cipher.nonce = initialization_vector
|
|
130
|
+
cipher.cipher.auth_tag = auth_tag.to_s
|
|
131
|
+
cipher.cipher.auth_data = ''
|
|
132
|
+
cipher.cipher.update(ciphertext.to_s) << cipher.cipher.final
|
|
112
133
|
end
|
|
113
134
|
|
|
114
135
|
#
|
|
@@ -117,7 +138,7 @@ module Net
|
|
|
117
138
|
# N_MAX maximum nonce (IV) length 12 octets
|
|
118
139
|
#
|
|
119
140
|
def iv_len
|
|
120
|
-
|
|
141
|
+
GCMCipher::IV_BYTES
|
|
121
142
|
end
|
|
122
143
|
|
|
123
144
|
#
|
|
@@ -201,7 +222,7 @@ module Net
|
|
|
201
222
|
end
|
|
202
223
|
# rubocop:enable Metrics/BlockLength
|
|
203
224
|
end
|
|
204
|
-
# rubocop:enable Metrics/AbcSize
|
|
225
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
205
226
|
end
|
|
206
227
|
end
|
|
207
228
|
end
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require 'x25519'
|
|
4
|
-
|
|
1
|
+
require 'openssl'
|
|
5
2
|
require 'net/ssh/transport/constants'
|
|
6
3
|
require 'net/ssh/transport/kex/abstract5656'
|
|
7
4
|
|
|
@@ -12,25 +9,60 @@ module Net
|
|
|
12
9
|
# A key-exchange service implementing the "curve25519-sha256@libssh.org"
|
|
13
10
|
# key-exchange algorithm. (defined in https://tools.ietf.org/html/draft-ietf-curdle-ssh-curves-06)
|
|
14
11
|
class Curve25519Sha256 < Abstract5656
|
|
12
|
+
ALGORITHM = "X25519"
|
|
13
|
+
KEY_BYTES = 32
|
|
14
|
+
|
|
15
|
+
class UnsupportedError < StandardError; end
|
|
16
|
+
|
|
17
|
+
def self.ensure_supported!
|
|
18
|
+
unless OpenSSL::PKey.respond_to?(:generate_key) && OpenSSL::PKey.respond_to?(:new_raw_public_key)
|
|
19
|
+
raise UnsupportedError, "OpenSSL::PKey raw public key APIs are unavailable"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
key = generate_x25519_key
|
|
23
|
+
public_key = new_raw_public_key(key.raw_public_key)
|
|
24
|
+
validate_key_bytes!(key.derive(public_key), "shared secret", KEY_BYTES)
|
|
25
|
+
rescue OpenSSL::PKey::PKeyError => e
|
|
26
|
+
raise UnsupportedError, e.message
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.generate_x25519_key
|
|
30
|
+
OpenSSL::PKey.generate_key(ALGORITHM)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.new_raw_public_key(key)
|
|
34
|
+
validate_key_bytes!(key, "public key", KEY_BYTES)
|
|
35
|
+
OpenSSL::PKey.new_raw_public_key(ALGORITHM, binary_string(key))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.validate_key_bytes!(key, label, expected_bytes)
|
|
39
|
+
raise ArgumentError, "invalid X25519 #{label}" unless key.respond_to?(:bytesize) && key.bytesize == expected_bytes
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.binary_string(string)
|
|
43
|
+
string.dup.force_encoding('BINARY')
|
|
44
|
+
end
|
|
45
|
+
private_class_method :binary_string
|
|
46
|
+
|
|
15
47
|
def digester
|
|
16
48
|
OpenSSL::Digest::SHA256
|
|
17
49
|
end
|
|
18
50
|
|
|
19
51
|
private
|
|
20
52
|
|
|
21
|
-
def generate_key
|
|
22
|
-
|
|
53
|
+
def generate_key
|
|
54
|
+
self.class.generate_x25519_key
|
|
23
55
|
end
|
|
24
56
|
|
|
25
57
|
## string Q_C, client's ephemeral public key octet string
|
|
26
58
|
def ecdh_public_key_bytes
|
|
27
|
-
ecdh.
|
|
59
|
+
ecdh.raw_public_key
|
|
28
60
|
end
|
|
29
61
|
|
|
30
62
|
# compute shared secret from server's public key and client's private key
|
|
31
63
|
def compute_shared_secret(server_ecdh_pubkey)
|
|
32
|
-
pk =
|
|
33
|
-
OpenSSL::BN.new(ecdh.
|
|
64
|
+
pk = self.class.new_raw_public_key(server_ecdh_pubkey)
|
|
65
|
+
OpenSSL::BN.new(ecdh.derive(pk), 2)
|
|
34
66
|
end
|
|
35
67
|
end
|
|
36
68
|
end
|
|
@@ -2,26 +2,34 @@ module Net
|
|
|
2
2
|
module SSH
|
|
3
3
|
module Transport
|
|
4
4
|
module Kex
|
|
5
|
-
# Loads Curve25519Sha256 support
|
|
5
|
+
# Loads Curve25519Sha256 support backed by Ruby OpenSSL X25519 APIs.
|
|
6
6
|
module Curve25519Sha256Loader
|
|
7
7
|
begin
|
|
8
8
|
require 'net/ssh/transport/kex/curve25519_sha256'
|
|
9
|
+
Net::SSH::Transport::Kex::Curve25519Sha256.ensure_supported!
|
|
9
10
|
LOADED = true
|
|
10
11
|
ERROR = nil
|
|
11
12
|
rescue LoadError => e
|
|
12
13
|
ERROR = e
|
|
13
14
|
LOADED = false
|
|
15
|
+
rescue StandardError => e
|
|
16
|
+
if defined?(Net::SSH::Transport::Kex::Curve25519Sha256::UnsupportedError) &&
|
|
17
|
+
e.is_a?(Net::SSH::Transport::Kex::Curve25519Sha256::UnsupportedError)
|
|
18
|
+
ERROR = e
|
|
19
|
+
LOADED = false
|
|
20
|
+
else
|
|
21
|
+
raise
|
|
22
|
+
end
|
|
14
23
|
end
|
|
15
24
|
|
|
16
25
|
def self.raiseUnlessLoaded(message)
|
|
17
|
-
description =
|
|
18
|
-
description
|
|
26
|
+
description = LOADED ? '' : x25519SupportRequired
|
|
27
|
+
description += "#{ERROR.class} : \"#{ERROR.message}\"\n" if ERROR
|
|
19
28
|
raise NotImplementedError, "#{message}\n#{description}" unless LOADED
|
|
20
29
|
end
|
|
21
30
|
|
|
22
|
-
def self.
|
|
23
|
-
|
|
24
|
-
result << " * x25519\n"
|
|
31
|
+
def self.x25519SupportRequired
|
|
32
|
+
"net-ssh requires openssl >= 3.2.0 and a crypto backend with X25519 enabled for curve25519-sha256 support.\n"
|
|
25
33
|
end
|
|
26
34
|
end
|
|
27
35
|
end
|
|
@@ -15,9 +15,6 @@ module Net
|
|
|
15
15
|
module PacketStream # rubocop:disable Metrics/ModuleLength
|
|
16
16
|
PROXY_COMMAND_HOST_IP = '<no hostip for proxy command>'.freeze
|
|
17
17
|
|
|
18
|
-
# Same limit as OpenSSH; a larger length would make us buffer whatever the server streams (GHSA-pfqh-p9vh-wx4p).
|
|
19
|
-
PACKET_MAX_SIZE = 256 * 1024
|
|
20
|
-
|
|
21
18
|
include BufferedIo
|
|
22
19
|
|
|
23
20
|
def self.extended(object)
|
|
@@ -243,7 +240,6 @@ module Net
|
|
|
243
240
|
@packet = Net::SSH::Buffer.new(server.update_cipher(data))
|
|
244
241
|
@packet_length = @packet.read_long
|
|
245
242
|
end
|
|
246
|
-
raise Net::SSH::Exception, "bad packet length #{@packet_length}" if @packet_length < 5 || @packet_length > PACKET_MAX_SIZE
|
|
247
243
|
end
|
|
248
244
|
|
|
249
245
|
need = @packet_length + 4 - aad_length - server.block_size
|
|
@@ -18,10 +18,6 @@ module Net
|
|
|
18
18
|
# The SSH version string as reported by Net::SSH
|
|
19
19
|
PROTO_VERSION = "SSH-2.0-Ruby/Net::SSH_#{Net::SSH::Version::CURRENT} #{RUBY_PLATFORM}"
|
|
20
20
|
|
|
21
|
-
# Same limits as OpenSSH, so a server can't make us buffer its banner without bound (GHSA-r75w-7fhp-84w8).
|
|
22
|
-
MAX_LINE_LENGTH = 8192
|
|
23
|
-
MAX_HEADER_LINES = 1024
|
|
24
|
-
|
|
25
21
|
# Any header text sent by the server prior to sending the version.
|
|
26
22
|
attr_reader :header
|
|
27
23
|
|
|
@@ -49,15 +45,24 @@ module Net
|
|
|
49
45
|
socket.write "#{PROTO_VERSION}\r\n"
|
|
50
46
|
socket.flush
|
|
51
47
|
|
|
52
|
-
|
|
48
|
+
raise Net::SSH::ConnectionTimeout, "timeout during server version negotiating" if timeout && !IO.select([socket], nil, nil, timeout)
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
@version =
|
|
50
|
+
loop do
|
|
51
|
+
@version = String.new
|
|
52
|
+
loop do
|
|
53
|
+
begin
|
|
54
|
+
b = socket.readpartial(1)
|
|
55
|
+
raise Net::SSH::Disconnect, "connection closed by remote host" if b.nil?
|
|
56
|
+
rescue EOFError
|
|
57
|
+
raise Net::SSH::Disconnect, "connection closed by remote host"
|
|
58
|
+
end
|
|
59
|
+
@version << b
|
|
60
|
+
break if b == "\n"
|
|
61
|
+
end
|
|
56
62
|
break if @version.match(/^SSH-/)
|
|
57
63
|
|
|
58
64
|
@header << @version
|
|
59
65
|
end
|
|
60
|
-
raise Net::SSH::Exception, "no SSH version received in the first #{MAX_HEADER_LINES} lines" unless @version.match(/^SSH-/)
|
|
61
66
|
|
|
62
67
|
@version.chomp!
|
|
63
68
|
debug { "remote is `#{@version}'" }
|
|
@@ -66,31 +71,6 @@ module Net
|
|
|
66
71
|
|
|
67
72
|
raise Net::SSH::ConnectionTimeout, "timeout during client version negotiating" if timeout && !IO.select(nil, [socket], nil, timeout)
|
|
68
73
|
end
|
|
69
|
-
|
|
70
|
-
def read_line(socket, deadline)
|
|
71
|
-
line = String.new
|
|
72
|
-
loop do
|
|
73
|
-
wait_readable(socket, deadline)
|
|
74
|
-
begin
|
|
75
|
-
b = socket.readpartial(1)
|
|
76
|
-
raise Net::SSH::Disconnect, "connection closed by remote host" if b.nil?
|
|
77
|
-
rescue EOFError
|
|
78
|
-
raise Net::SSH::Disconnect, "connection closed by remote host"
|
|
79
|
-
end
|
|
80
|
-
line << b
|
|
81
|
-
return line if b == "\n"
|
|
82
|
-
raise Net::SSH::Exception, "server version line exceeds #{MAX_LINE_LENGTH} bytes" if line.bytesize > MAX_LINE_LENGTH
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def wait_readable(socket, deadline)
|
|
87
|
-
return unless deadline
|
|
88
|
-
|
|
89
|
-
remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
90
|
-
return if remaining > 0 && IO.select([socket], nil, nil, remaining)
|
|
91
|
-
|
|
92
|
-
raise Net::SSH::ConnectionTimeout, "timeout during server version negotiating"
|
|
93
|
-
end
|
|
94
74
|
end
|
|
95
75
|
end
|
|
96
76
|
end
|
|
@@ -27,10 +27,6 @@ module Net
|
|
|
27
27
|
# The standard port for the SSH protocol.
|
|
28
28
|
DEFAULT_PORT = 22
|
|
29
29
|
|
|
30
|
-
# Packets held back during a re-key are bounded so a server can't exhaust memory (GHSA-ggxh-cfwq-5xqc).
|
|
31
|
-
MAX_QUEUED_PACKETS = 16_384
|
|
32
|
-
MAX_QUEUED_BYTES = 32 * 1024 * 1024
|
|
33
|
-
|
|
34
30
|
# The host to connect to, as given to the constructor.
|
|
35
31
|
attr_reader :host
|
|
36
32
|
|
|
@@ -83,7 +79,6 @@ module Net
|
|
|
83
79
|
debug { "connection established" }
|
|
84
80
|
|
|
85
81
|
@queue = []
|
|
86
|
-
@queued_bytes = 0
|
|
87
82
|
|
|
88
83
|
@host_key_verifier = select_host_key_verifier(options[:verify_host_key])
|
|
89
84
|
|
|
@@ -193,7 +188,7 @@ module Net
|
|
|
193
188
|
# read from the queue before the socket is queried.
|
|
194
189
|
def poll_message(mode = :nonblock, consume_queue = true)
|
|
195
190
|
loop do
|
|
196
|
-
return
|
|
191
|
+
return @queue.shift if consume_queue && @queue.any? && algorithms.allow?(@queue.first)
|
|
197
192
|
|
|
198
193
|
packet = socket.next_packet(mode, options[:timeout])
|
|
199
194
|
return nil if packet.nil?
|
|
@@ -216,7 +211,6 @@ module Net
|
|
|
216
211
|
|
|
217
212
|
else
|
|
218
213
|
return packet if algorithms.allow?(packet)
|
|
219
|
-
raise Net::SSH::Exception, "unexpected packet type #{packet.type} during initial key exchange" unless algorithms.initialized?
|
|
220
214
|
|
|
221
215
|
push(packet)
|
|
222
216
|
end
|
|
@@ -240,11 +234,6 @@ module Net
|
|
|
240
234
|
# #poll_message will return packets from the queue in the order they
|
|
241
235
|
# were received.
|
|
242
236
|
def push(packet)
|
|
243
|
-
@queued_bytes += packet.length
|
|
244
|
-
if @queue.size >= MAX_QUEUED_PACKETS || @queued_bytes > MAX_QUEUED_BYTES
|
|
245
|
-
raise Net::SSH::Exception, "too many packets queued during key exchange"
|
|
246
|
-
end
|
|
247
|
-
|
|
248
237
|
@queue.push(packet)
|
|
249
238
|
end
|
|
250
239
|
|
|
@@ -287,12 +276,6 @@ module Net
|
|
|
287
276
|
|
|
288
277
|
private
|
|
289
278
|
|
|
290
|
-
def shift_queue
|
|
291
|
-
packet = @queue.shift
|
|
292
|
-
@queued_bytes -= packet.length
|
|
293
|
-
packet
|
|
294
|
-
end
|
|
295
|
-
|
|
296
279
|
# Compatibility verifier which allows users to keep using
|
|
297
280
|
# custom verifier code without adding new :verify_signature
|
|
298
281
|
# method.
|