net-ssh 7.2.3 → 8.0.4
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/.dockerignore +3 -0
- data/.github/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +18 -0
- data/.github/workflows/ci-with-docker.yml +20 -9
- data/.github/workflows/ci.yml +10 -20
- data/.github/workflows/rubocop.yml +3 -1
- data/.rubocop.yml +5 -1
- data/.rubocop_todo.yml +4 -8
- data/CHANGES.txt +122 -0
- data/CONTRIBUTING.md +34 -0
- data/Dockerfile +4 -1
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +29 -0
- data/Gemfile +2 -0
- data/README.md +74 -57
- data/Rakefile +30 -12
- data/appveyor.yml +4 -16
- data/changelog.d/README.md +1 -0
- data/docker-compose.yml +1 -9
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/ed25519.rb +140 -37
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +42 -16
- data/lib/net/ssh/authentication/methods/abstract.rb +2 -8
- data/lib/net/ssh/authentication/methods/publickey.rb +3 -1
- data/lib/net/ssh/authentication/security_key.rb +71 -0
- data/lib/net/ssh/authentication/session.rb +29 -17
- data/lib/net/ssh/buffer.rb +6 -1
- data/lib/net/ssh/buffered_io.rb +40 -3
- data/lib/net/ssh/config.rb +30 -13
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +151 -13
- data/lib/net/ssh/errors.rb +24 -6
- data/lib/net/ssh/key_factory.rb +11 -4
- data/lib/net/ssh/known_hosts.rb +62 -8
- data/lib/net/ssh/multiplex/client.rb +162 -0
- data/lib/net/ssh/multiplex/control_path.rb +120 -0
- data/lib/net/ssh/multiplex/proxy_transport.rb +218 -0
- data/lib/net/ssh/multiplex.rb +126 -0
- data/lib/net/ssh/prompt.rb +3 -3
- data/lib/net/ssh/proxy/command.rb +28 -3
- data/lib/net/ssh/proxy/jump.rb +71 -8
- data/lib/net/ssh/service/forward.rb +44 -27
- data/lib/net/ssh/test/extensions.rb +3 -2
- data/lib/net/ssh/test/socket.rb +1 -1
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/aes128_gcm.rb +40 -0
- data/lib/net/ssh/transport/aes256_gcm.rb +40 -0
- data/lib/net/ssh/transport/algorithms.rb +96 -34
- 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 +32 -8
- data/lib/net/ssh/transport/constants.rb +1 -0
- data/lib/net/ssh/transport/gcm_cipher.rb +228 -0
- data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
- data/lib/net/ssh/transport/hmac/sha1_etm.rb +12 -0
- data/lib/net/ssh/transport/hmac.rb +2 -0
- 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/kex/diffie_hellman_group1_sha1.rb +8 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +45 -15
- data/lib/net/ssh/transport/openssl.rb +2 -1
- data/lib/net/ssh/transport/packet_stream.rb +19 -3
- data/lib/net/ssh/transport/server_version.rb +33 -13
- data/lib/net/ssh/transport/session.rb +61 -46
- data/lib/net/ssh/transport/state.rb +1 -1
- data/lib/net/ssh/verifiers/always.rb +43 -8
- data/lib/net/ssh/version.rb +3 -3
- data/lib/net/ssh.rb +148 -63
- data/net-ssh-public_cert.pem +7 -7
- data/net-ssh.gemspec +6 -8
- data.tar.gz.sig +0 -0
- metadata +64 -46
- metadata.gz.sig +0 -0
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
require 'net/ssh/loggable'
|
|
2
|
+
|
|
3
|
+
module Net
|
|
4
|
+
module SSH
|
|
5
|
+
module Transport
|
|
6
|
+
## Extension module for aes(128|256)gcm ciphers
|
|
7
|
+
module GCMCipher
|
|
8
|
+
BLOCK_SIZE = 16
|
|
9
|
+
IV_BYTES = 12
|
|
10
|
+
AUTH_TAG_BYTES = 16
|
|
11
|
+
|
|
12
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
13
|
+
def self.extended(orig)
|
|
14
|
+
# rubocop:disable Metrics/BlockLength
|
|
15
|
+
orig.class_eval do
|
|
16
|
+
include Net::SSH::Loggable
|
|
17
|
+
|
|
18
|
+
attr_reader :cipher
|
|
19
|
+
attr_reader :key
|
|
20
|
+
attr_reader :nonce
|
|
21
|
+
|
|
22
|
+
#
|
|
23
|
+
# Semantically gcm cipher supplies the OpenSSL iv interface with a nonce
|
|
24
|
+
# as it is not randomly generated due to being supplied from a counter.
|
|
25
|
+
# The RFC's use IV and nonce interchangeably.
|
|
26
|
+
#
|
|
27
|
+
def initialize(encrypt:, key:)
|
|
28
|
+
@cipher = OpenSSL::Cipher.new(algo_name)
|
|
29
|
+
@key = key
|
|
30
|
+
key_len = @cipher.key_len
|
|
31
|
+
if key.size != key_len
|
|
32
|
+
error_message = "#{cipher_name}: keylength does not match"
|
|
33
|
+
error { error_message }
|
|
34
|
+
raise error_message
|
|
35
|
+
end
|
|
36
|
+
encrypt ? @cipher.encrypt : @cipher.decrypt
|
|
37
|
+
@cipher.key = key
|
|
38
|
+
|
|
39
|
+
@nonce = {
|
|
40
|
+
fixed: nil,
|
|
41
|
+
invocation_counter: 0
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def update_cipher_mac(payload, _sequence_number)
|
|
46
|
+
#
|
|
47
|
+
# --- RFC 5647 7.3 ---
|
|
48
|
+
# When using AES-GCM with secure shell, the packet_length field is to
|
|
49
|
+
# be treated as additional authenticated data, not as plaintext.
|
|
50
|
+
#
|
|
51
|
+
length_data = [payload.bytesize].pack('N')
|
|
52
|
+
|
|
53
|
+
cipher.auth_data = length_data
|
|
54
|
+
|
|
55
|
+
encrypted_data = cipher.update(payload) << cipher.final
|
|
56
|
+
|
|
57
|
+
mac = cipher.auth_tag
|
|
58
|
+
|
|
59
|
+
incr_nonce
|
|
60
|
+
length_data + encrypted_data + mac
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
#
|
|
64
|
+
# --- RFC 5647 ---
|
|
65
|
+
# uint32 packet_length; // 0 <= packet_length < 2^32
|
|
66
|
+
#
|
|
67
|
+
def read_length(data, _sequence_number)
|
|
68
|
+
data.unpack1('N')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#
|
|
72
|
+
# --- RFC 5647 ---
|
|
73
|
+
# In AES-GCM secure shell, the inputs to the authenticated encryption
|
|
74
|
+
# are:
|
|
75
|
+
# PT (Plain Text)
|
|
76
|
+
# byte padding_length; // 4 <= padding_length < 256
|
|
77
|
+
# byte[n1] payload; // n1 = packet_length-padding_length-1
|
|
78
|
+
# byte[n2] random_padding; // n2 = padding_length
|
|
79
|
+
# AAD (Additional Authenticated Data)
|
|
80
|
+
# uint32 packet_length; // 0 <= packet_length < 2^32
|
|
81
|
+
# IV (Initialization Vector)
|
|
82
|
+
# As described in section 7.1.
|
|
83
|
+
# BK (Block Cipher Key)
|
|
84
|
+
# The appropriate Encryption Key formed during the Key Exchange.
|
|
85
|
+
#
|
|
86
|
+
def read_and_mac(data, mac, _sequence_number)
|
|
87
|
+
# The authentication tag will be placed in the MAC field at the end of the packet
|
|
88
|
+
|
|
89
|
+
# OpenSSL does not verify auth tag length
|
|
90
|
+
# GCM mode allows arbitrary sizes for the auth_tag up to 128 bytes and a single
|
|
91
|
+
# byte allows authentication to pass. If single byte auth tags are possible
|
|
92
|
+
# an attacker would require no more than 256 attempts to forge a valid tag.
|
|
93
|
+
#
|
|
94
|
+
raise 'incorrect auth_tag length' unless mac.to_s.length == mac_length
|
|
95
|
+
|
|
96
|
+
packet_length = data.unpack1('N')
|
|
97
|
+
|
|
98
|
+
cipher.auth_tag = mac.to_s
|
|
99
|
+
cipher.auth_data = [packet_length].pack('N')
|
|
100
|
+
|
|
101
|
+
result = cipher.update(data[4...]) << cipher.final
|
|
102
|
+
incr_nonce
|
|
103
|
+
result
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def mac_length
|
|
107
|
+
GCMCipher::AUTH_TAG_BYTES
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def block_size
|
|
111
|
+
GCMCipher::BLOCK_SIZE
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.block_size
|
|
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
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
#
|
|
136
|
+
# --- RFC 5647 ---
|
|
137
|
+
# N_MIN minimum nonce (IV) length 12 octets
|
|
138
|
+
# N_MAX maximum nonce (IV) length 12 octets
|
|
139
|
+
#
|
|
140
|
+
def iv_len
|
|
141
|
+
GCMCipher::IV_BYTES
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
#
|
|
145
|
+
# --- RFC 5288 ---
|
|
146
|
+
# Each value of the nonce_explicit MUST be distinct for each distinct
|
|
147
|
+
# invocation of the GCM encrypt function for any fixed key. Failure to
|
|
148
|
+
# meet this uniqueness requirement can significantly degrade security.
|
|
149
|
+
# The nonce_explicit MAY be the 64-bit sequence number.
|
|
150
|
+
#
|
|
151
|
+
# --- RFC 5116 ---
|
|
152
|
+
# (2.1) Applications that can generate distinct nonces SHOULD use the nonce
|
|
153
|
+
# formation method defined in Section 3.2, and MAY use any
|
|
154
|
+
# other method that meets the uniqueness requirement.
|
|
155
|
+
#
|
|
156
|
+
# (3.2) The following method to construct nonces is RECOMMENDED.
|
|
157
|
+
#
|
|
158
|
+
# <- variable -> <- variable ->
|
|
159
|
+
# - - - - - - - - - - - - - -
|
|
160
|
+
# | fixed | counter |
|
|
161
|
+
#
|
|
162
|
+
# Initial octets consist of a fixed field and final octets consist of a
|
|
163
|
+
# Counter field. Implementations SHOULD support 12-octet nonces in which
|
|
164
|
+
# the Counter field is four octets long.
|
|
165
|
+
# The Counter fields of successive nonces form a monotonically increasing
|
|
166
|
+
# sequence, when those fields are regarded as unsignd integers in network
|
|
167
|
+
# byte order.
|
|
168
|
+
# The Counter part SHOULD be equal to zero for the first nonce and increment
|
|
169
|
+
# by one for each successive nonce that is generated.
|
|
170
|
+
# The Fixed field MUST remain constant for all nonces that are generated for
|
|
171
|
+
# a given encryption device.
|
|
172
|
+
#
|
|
173
|
+
# --- RFC 5647 ---
|
|
174
|
+
# The invocation field is treated as a 64-bit integer and is increment after
|
|
175
|
+
# each invocation of AES-GCM to process a binary packet.
|
|
176
|
+
# AES-GCM produces a keystream in blocks of 16-octets that is used to
|
|
177
|
+
# encrypt the plaintext. This keystream is produced by encrypting the
|
|
178
|
+
# following 16-octet data structure:
|
|
179
|
+
#
|
|
180
|
+
# uint32 fixed; // 4 octets
|
|
181
|
+
# uint64 invocation_counter; // 8 octets
|
|
182
|
+
# unit32 block_counter; // 4 octets
|
|
183
|
+
#
|
|
184
|
+
# The block_counter is initially set to one (1) and increment as each block
|
|
185
|
+
# of key is produced.
|
|
186
|
+
#
|
|
187
|
+
# The reader is reminded that SSH requires that the data to be encrypted
|
|
188
|
+
# MUST be padded out to a multiple of the block size (16-octets for AES-GCM).
|
|
189
|
+
#
|
|
190
|
+
def incr_nonce
|
|
191
|
+
return if nonce[:fixed].nil?
|
|
192
|
+
|
|
193
|
+
nonce[:invocation_counter] = [nonce[:invocation_counter].to_s.unpack1('B*').to_i(2) + 1].pack('Q>*')
|
|
194
|
+
|
|
195
|
+
apply_nonce
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def nonce=(iv_s)
|
|
199
|
+
return if nonce[:fixed]
|
|
200
|
+
|
|
201
|
+
nonce[:fixed] = iv_s[0...4]
|
|
202
|
+
nonce[:invocation_counter] = iv_s[4...12]
|
|
203
|
+
|
|
204
|
+
apply_nonce
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def apply_nonce
|
|
208
|
+
cipher.iv = "#{nonce[:fixed]}#{nonce[:invocation_counter]}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
#
|
|
212
|
+
# --- RFC 5647 ---
|
|
213
|
+
# If AES-GCM is selected as the encryption algorithm for a given
|
|
214
|
+
# tunnel, AES-GCM MUST also be selected as the Message Authentication
|
|
215
|
+
# Code (MAC) algorithm. Conversely, if AES-GCM is selected as the MAC
|
|
216
|
+
# algorithm, it MUST also be selected as the encryption algorithm.
|
|
217
|
+
#
|
|
218
|
+
def implicit_mac?
|
|
219
|
+
true
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
# rubocop:enable Metrics/BlockLength
|
|
224
|
+
end
|
|
225
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
@@ -8,6 +8,18 @@ module Net
|
|
|
8
8
|
# The base class of all OpenSSL-based HMAC algorithm wrappers.
|
|
9
9
|
class Abstract
|
|
10
10
|
class << self
|
|
11
|
+
def aead(*v)
|
|
12
|
+
@aead = false if !defined?(@aead)
|
|
13
|
+
if v.empty?
|
|
14
|
+
@aead = superclass.aead if @aead.nil? && superclass.respond_to?(:aead)
|
|
15
|
+
return @aead
|
|
16
|
+
elsif v.length == 1
|
|
17
|
+
@aead = v.first
|
|
18
|
+
else
|
|
19
|
+
raise ArgumentError, "wrong number of arguments (#{v.length} for 1)"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
11
23
|
def etm(*v)
|
|
12
24
|
@etm = false if !defined?(@etm)
|
|
13
25
|
if v.empty?
|
|
@@ -57,6 +69,10 @@ module Net
|
|
|
57
69
|
end
|
|
58
70
|
end
|
|
59
71
|
|
|
72
|
+
def aead
|
|
73
|
+
self.class.aead
|
|
74
|
+
end
|
|
75
|
+
|
|
60
76
|
def etm
|
|
61
77
|
self.class.etm
|
|
62
78
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
require 'net/ssh/transport/hmac/abstract'
|
|
2
|
+
|
|
3
|
+
module Net::SSH::Transport::HMAC
|
|
4
|
+
# The SHA1 Encrypt-Then-Mac HMAC algorithm. This has a mac and
|
|
5
|
+
# key length of 20, and uses the SHA1 digest algorithm.
|
|
6
|
+
class SHA1_Etm < Abstract
|
|
7
|
+
etm true
|
|
8
|
+
mac_length 20
|
|
9
|
+
key_length 20
|
|
10
|
+
digest_class OpenSSL::Digest::SHA1
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -9,6 +9,7 @@ require 'net/ssh/transport/hmac/sha2_512'
|
|
|
9
9
|
require 'net/ssh/transport/hmac/sha2_512_96'
|
|
10
10
|
require 'net/ssh/transport/hmac/sha2_256_etm'
|
|
11
11
|
require 'net/ssh/transport/hmac/sha2_512_etm'
|
|
12
|
+
require 'net/ssh/transport/hmac/sha1_etm'
|
|
12
13
|
require 'net/ssh/transport/hmac/ripemd160'
|
|
13
14
|
require 'net/ssh/transport/hmac/none'
|
|
14
15
|
|
|
@@ -27,6 +28,7 @@ module Net::SSH::Transport::HMAC
|
|
|
27
28
|
'hmac-sha2-512-96' => SHA2_512_96,
|
|
28
29
|
'hmac-sha2-256-etm@openssh.com' => SHA2_256_Etm,
|
|
29
30
|
'hmac-sha2-512-etm@openssh.com' => SHA2_512_Etm,
|
|
31
|
+
'hmac-sha1-etm@openssh.com' => SHA1_Etm,
|
|
30
32
|
'hmac-ripemd160' => RIPEMD160,
|
|
31
33
|
'hmac-ripemd160@openssh.com' => RIPEMD160,
|
|
32
34
|
'none' => None
|
|
@@ -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
|
|
@@ -102,6 +102,7 @@ module Net
|
|
|
102
102
|
result[:key_blob] = buffer.read_string
|
|
103
103
|
result[:server_key] = Net::SSH::Buffer.new(result[:key_blob]).read_key
|
|
104
104
|
result[:server_dh_pubkey] = buffer.read_bignum
|
|
105
|
+
validate_server_dh_pubkey(result[:server_dh_pubkey])
|
|
105
106
|
result[:shared_secret] = OpenSSL::BN.new(dh.compute_key(result[:server_dh_pubkey]), 2)
|
|
106
107
|
|
|
107
108
|
sig_buffer = Net::SSH::Buffer.new(buffer.read_string)
|
|
@@ -115,6 +116,13 @@ module Net
|
|
|
115
116
|
|
|
116
117
|
return result
|
|
117
118
|
end
|
|
119
|
+
|
|
120
|
+
# Rejects degenerate values that would let the server force a
|
|
121
|
+
# predictable shared secret, as OpenSSH's dh_pub_is_valid.
|
|
122
|
+
def validate_server_dh_pubkey(pub)
|
|
123
|
+
valid = pub > 1 && pub < dh.p - 1 && pub.to_i.to_s(2).count("1") >= 4
|
|
124
|
+
raise Net::SSH::Exception, "server's DH public value is invalid" unless valid
|
|
125
|
+
end
|
|
118
126
|
end
|
|
119
127
|
end
|
|
120
128
|
end
|
|
@@ -6,26 +6,43 @@ module Net::SSH::Transport::Kex
|
|
|
6
6
|
# A key-exchange service implementing the
|
|
7
7
|
# "diffie-hellman-group-exchange-sha1" key-exchange algorithm.
|
|
8
8
|
class DiffieHellmanGroupExchangeSHA1 < DiffieHellmanGroup1SHA1
|
|
9
|
-
MINIMUM_BITS =
|
|
9
|
+
MINIMUM_BITS = 2048
|
|
10
10
|
MAXIMUM_BITS = 8192
|
|
11
|
+
CISCO_MAXIMUM_NEED_BITS = 4096
|
|
11
12
|
|
|
12
13
|
private
|
|
13
14
|
|
|
14
|
-
#
|
|
15
|
+
# Computes the minimum, preferred and maximum group sizes to request,
|
|
16
|
+
# like OpenSSH's kexgex_client.
|
|
15
17
|
def compute_need_bits
|
|
16
|
-
# for Compatibility: OpenSSH requires (need_bits * 2 + 1) length of parameter
|
|
17
|
-
need_bits = data[:need_bytes] * 8 * 2 + 1
|
|
18
|
-
|
|
19
18
|
data[:minimum_dh_bits] ||= MINIMUM_BITS
|
|
19
|
+
data[:maximum_dh_bits] = [MAXIMUM_BITS, data[:minimum_dh_bits]].max
|
|
20
|
+
data[:need_bits] = estimate_group_bits(data[:need_bytes] * 8).clamp(data[:minimum_dh_bits], maximum_need_bits)
|
|
21
|
+
end
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
# Some Cisco servers fail instead of returning their 4096-bit group when asked for more (OpenSSH bz#2209).
|
|
24
|
+
def maximum_need_bits
|
|
25
|
+
return data[:maximum_dh_bits] unless cisco_server?
|
|
26
|
+
|
|
27
|
+
[CISCO_MAXIMUM_NEED_BITS, data[:minimum_dh_bits]].max
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def cisco_server?
|
|
31
|
+
data[:server_version_string].to_s.match?(/\ASSH-[\d.]+-Cisco-1\./)
|
|
32
|
+
end
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
# Group size giving at least the given bits of security (NIST SP 800-57),
|
|
35
|
+
# as OpenSSH's dh_estimate.
|
|
36
|
+
def estimate_group_bits(security_bits)
|
|
37
|
+
if security_bits <= 112
|
|
38
|
+
2048
|
|
39
|
+
elsif security_bits <= 128
|
|
40
|
+
3072
|
|
41
|
+
elsif security_bits <= 192
|
|
42
|
+
7680
|
|
43
|
+
else
|
|
44
|
+
8192
|
|
45
|
+
end
|
|
29
46
|
end
|
|
30
47
|
|
|
31
48
|
# Returns the DH key parameters for the given session.
|
|
@@ -34,7 +51,7 @@ module Net::SSH::Transport::Kex
|
|
|
34
51
|
|
|
35
52
|
# request the DH key parameters for the given number of bits.
|
|
36
53
|
buffer = Net::SSH::Buffer.from(:byte, KEXDH_GEX_REQUEST, :long, data[:minimum_dh_bits],
|
|
37
|
-
:long, data[:need_bits], :long,
|
|
54
|
+
:long, data[:need_bits], :long, data[:maximum_dh_bits])
|
|
38
55
|
connection.send_message(buffer)
|
|
39
56
|
|
|
40
57
|
buffer = connection.next_message
|
|
@@ -43,9 +60,22 @@ module Net::SSH::Transport::Kex
|
|
|
43
60
|
p = buffer.read_bignum
|
|
44
61
|
g = buffer.read_bignum
|
|
45
62
|
|
|
63
|
+
validate_group(p, g)
|
|
64
|
+
|
|
46
65
|
[p, g]
|
|
47
66
|
end
|
|
48
67
|
|
|
68
|
+
def validate_group(prime, generator)
|
|
69
|
+
bits = prime.num_bits
|
|
70
|
+
if bits < data[:minimum_dh_bits]
|
|
71
|
+
raise Net::SSH::Exception,
|
|
72
|
+
"server's DH group is #{bits} bits, below the minimum of #{data[:minimum_dh_bits]}; " \
|
|
73
|
+
"pass minimum_dh_bits: #{bits} to allow it"
|
|
74
|
+
end
|
|
75
|
+
raise Net::SSH::Exception, "server's DH group is #{bits} bits, above the maximum of #{data[:maximum_dh_bits]}" if bits > data[:maximum_dh_bits]
|
|
76
|
+
raise Net::SSH::Exception, "server's DH group generator is invalid" unless generator > 1 && generator < prime - 1
|
|
77
|
+
end
|
|
78
|
+
|
|
49
79
|
# Returns the INIT/REPLY constants used by this algorithm.
|
|
50
80
|
def get_message_types
|
|
51
81
|
[KEXDH_GEX_INIT, KEXDH_GEX_REPLY]
|
|
@@ -60,9 +90,9 @@ module Net::SSH::Transport::Kex
|
|
|
60
90
|
data[:client_algorithm_packet],
|
|
61
91
|
data[:server_algorithm_packet],
|
|
62
92
|
result[:key_blob]
|
|
63
|
-
response.write_long
|
|
93
|
+
response.write_long data[:minimum_dh_bits],
|
|
64
94
|
data[:need_bits],
|
|
65
|
-
|
|
95
|
+
data[:maximum_dh_bits]
|
|
66
96
|
response.write_bignum dh.p, dh.g, dh.pub_key,
|
|
67
97
|
result[:server_dh_pubkey],
|
|
68
98
|
result[:shared_secret]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'openssl'
|
|
2
|
+
require 'net/ssh/errors'
|
|
2
3
|
require 'net/ssh/authentication/pub_key_fingerprint'
|
|
3
4
|
|
|
4
5
|
module OpenSSL
|
|
@@ -178,7 +179,7 @@ module OpenSSL
|
|
|
178
179
|
|
|
179
180
|
return key
|
|
180
181
|
rescue OpenSSL::PKey::ECError
|
|
181
|
-
raise
|
|
182
|
+
raise Net::SSH::UnsupportedKeyType, "unsupported key type `ecdsa-sha2-#{curve_name_in_type}'"
|
|
182
183
|
end
|
|
183
184
|
end
|
|
184
185
|
|
|
@@ -15,6 +15,9 @@ 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
|
+
|
|
18
21
|
include BufferedIo
|
|
19
22
|
|
|
20
23
|
def self.extended(object)
|
|
@@ -128,7 +131,7 @@ module Net
|
|
|
128
131
|
payload = client.compress(payload)
|
|
129
132
|
|
|
130
133
|
# the length of the packet, minus the padding
|
|
131
|
-
actual_length = (client.hmac.etm ? 0 : 4) + payload.bytesize + 1
|
|
134
|
+
actual_length = (client.hmac.etm || client.hmac.aead ? 0 : 4) + payload.bytesize + 1
|
|
132
135
|
|
|
133
136
|
# compute the padding length
|
|
134
137
|
padding_length = client.block_size - (actual_length % client.block_size)
|
|
@@ -151,7 +154,7 @@ module Net
|
|
|
151
154
|
debug { "using encrypt-then-mac" }
|
|
152
155
|
|
|
153
156
|
# Encrypt padding_length, payload, and padding. Take MAC
|
|
154
|
-
# from the unencrypted
|
|
157
|
+
# from the unencrypted packet_length and the encrypted
|
|
155
158
|
# data.
|
|
156
159
|
length_data = [packet_length].pack("N")
|
|
157
160
|
|
|
@@ -187,6 +190,7 @@ module Net
|
|
|
187
190
|
def cleanup
|
|
188
191
|
client.cleanup
|
|
189
192
|
server.cleanup
|
|
193
|
+
hang_up_proxy_command
|
|
190
194
|
end
|
|
191
195
|
|
|
192
196
|
# If the IO object requires a rekey operation (as indicated by either its
|
|
@@ -202,6 +206,17 @@ module Net
|
|
|
202
206
|
|
|
203
207
|
protected
|
|
204
208
|
|
|
209
|
+
# Like OpenSSH, hang up a ProxyCommand child: tunnels such as EC2 Instance
|
|
210
|
+
# Connect don't exit on their own, and IO#close waits for them.
|
|
211
|
+
def hang_up_proxy_command
|
|
212
|
+
return unless respond_to?(:pid)
|
|
213
|
+
|
|
214
|
+
child = pid
|
|
215
|
+
Process.kill('HUP', child) if child
|
|
216
|
+
rescue IOError, Errno::ESRCH
|
|
217
|
+
# already closed, or the child has already been reaped
|
|
218
|
+
end
|
|
219
|
+
|
|
205
220
|
# Called when this module is used to extend an object. It initializes
|
|
206
221
|
# the states and generally prepares the object for use as a packet stream.
|
|
207
222
|
def initialize_ssh
|
|
@@ -219,7 +234,7 @@ module Net
|
|
|
219
234
|
# new Packet object.
|
|
220
235
|
# rubocop:disable Metrics/AbcSize
|
|
221
236
|
def poll_next_packet
|
|
222
|
-
aad_length = server.hmac.etm ? 4 : 0
|
|
237
|
+
aad_length = server.hmac.etm || server.hmac.aead ? 4 : 0
|
|
223
238
|
|
|
224
239
|
if @packet.nil?
|
|
225
240
|
minimum = server.block_size < 4 ? 4 : server.block_size
|
|
@@ -240,6 +255,7 @@ module Net
|
|
|
240
255
|
@packet = Net::SSH::Buffer.new(server.update_cipher(data))
|
|
241
256
|
@packet_length = @packet.read_long
|
|
242
257
|
end
|
|
258
|
+
raise Net::SSH::Exception, "bad packet length #{@packet_length}" if @packet_length < 5 || @packet_length > PACKET_MAX_SIZE
|
|
243
259
|
end
|
|
244
260
|
|
|
245
261
|
need = @packet_length + 4 - aad_length - server.block_size
|
|
@@ -18,6 +18,10 @@ 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
|
+
|
|
21
25
|
# Any header text sent by the server prior to sending the version.
|
|
22
26
|
attr_reader :header
|
|
23
27
|
|
|
@@ -45,24 +49,15 @@ module Net
|
|
|
45
49
|
socket.write "#{PROTO_VERSION}\r\n"
|
|
46
50
|
socket.flush
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
|
|
49
53
|
|
|
50
|
-
|
|
51
|
-
@version =
|
|
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
|
|
54
|
+
MAX_HEADER_LINES.times do
|
|
55
|
+
@version = read_line(socket, deadline)
|
|
62
56
|
break if @version.match(/^SSH-/)
|
|
63
57
|
|
|
64
58
|
@header << @version
|
|
65
59
|
end
|
|
60
|
+
raise Net::SSH::Exception, "no SSH version received in the first #{MAX_HEADER_LINES} lines" unless @version.match(/^SSH-/)
|
|
66
61
|
|
|
67
62
|
@version.chomp!
|
|
68
63
|
debug { "remote is `#{@version}'" }
|
|
@@ -71,6 +66,31 @@ module Net
|
|
|
71
66
|
|
|
72
67
|
raise Net::SSH::ConnectionTimeout, "timeout during client version negotiating" if timeout && !IO.select(nil, [socket], nil, timeout)
|
|
73
68
|
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
|
|
74
94
|
end
|
|
75
95
|
end
|
|
76
96
|
end
|