net-ssh 7.1.0 → 7.3.0
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/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +10 -3
- data/.gitignore +2 -0
- data/.rubocop_todo.yml +2 -2
- data/CHANGES.txt +24 -0
- data/DEVELOPMENT.md +23 -0
- data/Dockerfile +4 -2
- data/Gemfile.norbnacl +12 -0
- data/README.md +24 -18
- data/Rakefile +51 -18
- data/docker-compose.yml +2 -0
- data/lib/net/ssh/authentication/ed25519.rb +2 -4
- data/lib/net/ssh/authentication/key_manager.rb +19 -2
- data/lib/net/ssh/authentication/methods/publickey.rb +1 -1
- data/lib/net/ssh/authentication/pub_key_fingerprint.rb +1 -1
- data/lib/net/ssh/authentication/session.rb +7 -0
- data/lib/net/ssh/buffered_io.rb +1 -1
- data/lib/net/ssh/known_hosts.rb +2 -3
- 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 +35 -6
- data/lib/net/ssh/transport/chacha20_poly1305_cipher.rb +117 -0
- data/lib/net/ssh/transport/chacha20_poly1305_cipher_loader.rb +17 -0
- data/lib/net/ssh/transport/cipher_factory.rb +28 -1
- data/lib/net/ssh/transport/gcm_cipher.rb +207 -0
- data/lib/net/ssh/transport/hmac/abstract.rb +16 -0
- data/lib/net/ssh/transport/identity_cipher.rb +8 -0
- data/lib/net/ssh/transport/openssl_cipher_extensions.rb +8 -0
- data/lib/net/ssh/transport/packet_stream.rb +44 -23
- data/lib/net/ssh/transport/state.rb +1 -1
- data/lib/net/ssh/version.rb +1 -1
- data/lib/net/ssh.rb +5 -2
- data/net-ssh-public_cert.pem +19 -18
- data/net-ssh.gemspec +5 -2
- data.tar.gz.sig +0 -0
- metadata +62 -24
- metadata.gz.sig +0 -0
|
@@ -44,13 +44,22 @@ module Net
|
|
|
44
44
|
diffie-hellman-group14-sha256
|
|
45
45
|
diffie-hellman-group14-sha1],
|
|
46
46
|
|
|
47
|
-
encryption: %w[aes256-ctr
|
|
47
|
+
encryption: %w[aes256-ctr
|
|
48
|
+
aes192-ctr
|
|
49
|
+
aes128-ctr
|
|
50
|
+
aes256-gcm@openssh.com
|
|
51
|
+
aes128-gcm@openssh.com],
|
|
48
52
|
|
|
49
53
|
hmac: %w[hmac-sha2-512-etm@openssh.com hmac-sha2-256-etm@openssh.com
|
|
50
54
|
hmac-sha2-512 hmac-sha2-256
|
|
51
55
|
hmac-sha1]
|
|
52
56
|
}.freeze
|
|
53
57
|
|
|
58
|
+
if Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED
|
|
59
|
+
DEFAULT_ALGORITHMS[:encryption].unshift(
|
|
60
|
+
'chacha20-poly1305@openssh.com'
|
|
61
|
+
)
|
|
62
|
+
end
|
|
54
63
|
if Net::SSH::Authentication::ED25519Loader::LOADED
|
|
55
64
|
DEFAULT_ALGORITHMS[:host_key].unshift(
|
|
56
65
|
'ssh-ed25519-cert-v01@openssh.com',
|
|
@@ -437,12 +446,13 @@ module Net
|
|
|
437
446
|
def exchange_keys
|
|
438
447
|
debug { "exchanging keys" }
|
|
439
448
|
|
|
449
|
+
need_bytes = kex_byte_requirement
|
|
440
450
|
algorithm = Kex::MAP[kex].new(self, session,
|
|
441
451
|
client_version_string: Net::SSH::Transport::ServerVersion::PROTO_VERSION,
|
|
442
452
|
server_version_string: session.server_version.version,
|
|
443
453
|
server_algorithm_packet: @server_packet,
|
|
444
454
|
client_algorithm_packet: @client_packet,
|
|
445
|
-
need_bytes:
|
|
455
|
+
need_bytes: need_bytes,
|
|
446
456
|
minimum_dh_bits: options[:minimum_dh_bits],
|
|
447
457
|
logger: logger)
|
|
448
458
|
result = algorithm.exchange_keys
|
|
@@ -464,11 +474,30 @@ module Net
|
|
|
464
474
|
|
|
465
475
|
parameters = { shared: secret, hash: hash, digester: digester }
|
|
466
476
|
|
|
467
|
-
cipher_client = CipherFactory.get(
|
|
468
|
-
|
|
477
|
+
cipher_client = CipherFactory.get(
|
|
478
|
+
encryption_client,
|
|
479
|
+
parameters.merge(iv: iv_client, key: key_client, encrypt: true)
|
|
480
|
+
)
|
|
481
|
+
cipher_server = CipherFactory.get(
|
|
482
|
+
encryption_server,
|
|
483
|
+
parameters.merge(iv: iv_server, key: key_server, decrypt: true)
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
mac_client =
|
|
487
|
+
if cipher_client.implicit_mac?
|
|
488
|
+
cipher_client.implicit_mac
|
|
489
|
+
else
|
|
490
|
+
HMAC.get(hmac_client, mac_key_client, parameters)
|
|
491
|
+
end
|
|
492
|
+
mac_server =
|
|
493
|
+
if cipher_server.implicit_mac?
|
|
494
|
+
cipher_server.implicit_mac
|
|
495
|
+
else
|
|
496
|
+
HMAC.get(hmac_server, mac_key_server, parameters)
|
|
497
|
+
end
|
|
469
498
|
|
|
470
|
-
|
|
471
|
-
|
|
499
|
+
cipher_client.nonce = iv_client if mac_client.respond_to?(:aead) && mac_client.aead
|
|
500
|
+
cipher_server.nonce = iv_server if mac_server.respond_to?(:aead) && mac_client.aead
|
|
472
501
|
|
|
473
502
|
session.configure_client cipher: cipher_client, hmac: mac_client,
|
|
474
503
|
compression: normalize_compression_name(compression_client),
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
require 'rbnacl'
|
|
2
|
+
require 'net/ssh/loggable'
|
|
3
|
+
|
|
4
|
+
module Net
|
|
5
|
+
module SSH
|
|
6
|
+
module Transport
|
|
7
|
+
## Implements the chacha20-poly1305@openssh cipher
|
|
8
|
+
class ChaCha20Poly1305Cipher
|
|
9
|
+
include Net::SSH::Loggable
|
|
10
|
+
|
|
11
|
+
# Implicit HMAC, no need to do anything
|
|
12
|
+
class ImplicitHMac
|
|
13
|
+
def etm
|
|
14
|
+
# TODO: ideally this shouln't be called
|
|
15
|
+
true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def key_length
|
|
19
|
+
64
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(encrypt:, key:)
|
|
24
|
+
@chacha_hdr = OpenSSL::Cipher.new("chacha20")
|
|
25
|
+
key_len = @chacha_hdr.key_len
|
|
26
|
+
@chacha_main = OpenSSL::Cipher.new("chacha20")
|
|
27
|
+
@poly = RbNaCl::OneTimeAuths::Poly1305
|
|
28
|
+
if key.size < key_len * 2
|
|
29
|
+
error { "chacha20_poly1305: keylength doesn't match" }
|
|
30
|
+
raise "chacha20_poly1305: keylength doesn't match"
|
|
31
|
+
end
|
|
32
|
+
if encrypt
|
|
33
|
+
@chacha_hdr.encrypt
|
|
34
|
+
@chacha_main.encrypt
|
|
35
|
+
else
|
|
36
|
+
@chacha_hdr.decrypt
|
|
37
|
+
@chacha_main.decrypt
|
|
38
|
+
end
|
|
39
|
+
main_key = key[0...key_len]
|
|
40
|
+
@chacha_main.key = main_key
|
|
41
|
+
hdr_key = key[key_len...(2 * key_len)]
|
|
42
|
+
@chacha_hdr.key = hdr_key
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def update_cipher_mac(payload, sequence_number)
|
|
46
|
+
iv_data = [0, 0, 0, sequence_number].pack("NNNN")
|
|
47
|
+
@chacha_main.iv = iv_data
|
|
48
|
+
poly_key = @chacha_main.update(([0] * 32).pack('C32'))
|
|
49
|
+
|
|
50
|
+
packet_length = payload.size
|
|
51
|
+
length_data = [packet_length].pack("N")
|
|
52
|
+
@chacha_hdr.iv = iv_data
|
|
53
|
+
packet = @chacha_hdr.update(length_data)
|
|
54
|
+
|
|
55
|
+
iv_data[0] = 1.chr
|
|
56
|
+
@chacha_main.iv = iv_data
|
|
57
|
+
unencrypted_data = payload
|
|
58
|
+
packet += @chacha_main.update(unencrypted_data)
|
|
59
|
+
|
|
60
|
+
packet += @poly.auth(poly_key, packet)
|
|
61
|
+
return packet
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def read_length(data, sequence_number)
|
|
65
|
+
iv_data = [0, 0, 0, sequence_number].pack("NNNN")
|
|
66
|
+
@chacha_hdr.iv = iv_data
|
|
67
|
+
@chacha_hdr.update(data).unpack1("N")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def read_and_mac(data, mac, sequence_number)
|
|
71
|
+
iv_data = [0, 0, 0, sequence_number].pack("NNNN")
|
|
72
|
+
@chacha_main.iv = iv_data
|
|
73
|
+
poly_key = @chacha_main.update(([0] * 32).pack('C32'))
|
|
74
|
+
|
|
75
|
+
iv_data[0] = 1.chr
|
|
76
|
+
@chacha_main.iv = iv_data
|
|
77
|
+
unencrypted_data = @chacha_main.update(data[4..])
|
|
78
|
+
begin
|
|
79
|
+
ok = @poly.verify(poly_key, mac, data[0..])
|
|
80
|
+
raise Net::SSH::Exception, "corrupted hmac detected #{name}" unless ok
|
|
81
|
+
rescue RbNaCl::BadAuthenticatorError
|
|
82
|
+
raise Net::SSH::Exception, "corrupted hmac detected #{name}"
|
|
83
|
+
end
|
|
84
|
+
return unencrypted_data
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def mac_length
|
|
88
|
+
16
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def block_size
|
|
92
|
+
8
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def name
|
|
96
|
+
"chacha20-poly1305@openssh.com"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def implicit_mac?
|
|
100
|
+
true
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def implicit_mac
|
|
104
|
+
return ImplicitHMac.new
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.block_size
|
|
108
|
+
8
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.key_length
|
|
112
|
+
64
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Net
|
|
2
|
+
module SSH
|
|
3
|
+
module Transport
|
|
4
|
+
# Loads chacha20 poly1305 support which requires optinal dependency rbnacl
|
|
5
|
+
module ChaCha20Poly1305CipherLoader
|
|
6
|
+
begin
|
|
7
|
+
require 'net/ssh/transport/chacha20_poly1305_cipher'
|
|
8
|
+
LOADED = true
|
|
9
|
+
ERROR = nil
|
|
10
|
+
rescue LoadError => e
|
|
11
|
+
ERROR = e
|
|
12
|
+
LOADED = false
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
require 'openssl'
|
|
2
2
|
require 'net/ssh/transport/ctr.rb'
|
|
3
|
+
require 'net/ssh/transport/aes128_gcm'
|
|
4
|
+
require 'net/ssh/transport/aes256_gcm'
|
|
3
5
|
require 'net/ssh/transport/key_expander'
|
|
4
6
|
require 'net/ssh/transport/identity_cipher'
|
|
7
|
+
require 'net/ssh/transport/chacha20_poly1305_cipher_loader'
|
|
8
|
+
require 'net/ssh/transport/openssl_cipher_extensions'
|
|
5
9
|
|
|
6
10
|
module Net
|
|
7
11
|
module SSH
|
|
@@ -29,13 +33,25 @@ module Net
|
|
|
29
33
|
'none' => 'none'
|
|
30
34
|
}
|
|
31
35
|
|
|
36
|
+
SSH_TO_CLASS = {
|
|
37
|
+
'aes256-gcm@openssh.com' => Net::SSH::Transport::AES256_GCM,
|
|
38
|
+
'aes128-gcm@openssh.com' => Net::SSH::Transport::AES128_GCM
|
|
39
|
+
}.tap do |hash|
|
|
40
|
+
if Net::SSH::Transport::ChaCha20Poly1305CipherLoader::LOADED
|
|
41
|
+
hash['chacha20-poly1305@openssh.com'] =
|
|
42
|
+
Net::SSH::Transport::ChaCha20Poly1305Cipher
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
32
46
|
# Returns true if the underlying OpenSSL library supports the given cipher,
|
|
33
47
|
# and false otherwise.
|
|
34
48
|
def self.supported?(name)
|
|
49
|
+
return true if SSH_TO_CLASS.key?(name)
|
|
50
|
+
|
|
35
51
|
ossl_name = SSH_TO_OSSL[name] or raise NotImplementedError, "unimplemented cipher `#{name}'"
|
|
36
52
|
return true if ossl_name == "none"
|
|
37
53
|
|
|
38
|
-
return OpenSSL::Cipher.ciphers.include?(ossl_name)
|
|
54
|
+
return SSH_TO_CLASS.key?(name) || OpenSSL::Cipher.ciphers.include?(ossl_name)
|
|
39
55
|
end
|
|
40
56
|
|
|
41
57
|
# Retrieves a new instance of the named algorithm. The new instance
|
|
@@ -44,6 +60,13 @@ module Net
|
|
|
44
60
|
# cipher will be put into encryption or decryption mode, based on the
|
|
45
61
|
# value of the +encrypt+ parameter.
|
|
46
62
|
def self.get(name, options = {})
|
|
63
|
+
klass = SSH_TO_CLASS[name]
|
|
64
|
+
unless klass.nil?
|
|
65
|
+
key_len = klass.key_length
|
|
66
|
+
key = Net::SSH::Transport::KeyExpander.expand_key(key_len, options[:key], options)
|
|
67
|
+
return klass.new(encrypt: options[:encrypt], key: key)
|
|
68
|
+
end
|
|
69
|
+
|
|
47
70
|
ossl_name = SSH_TO_OSSL[name] or raise NotImplementedError, "unimplemented cipher `#{name}'"
|
|
48
71
|
return IdentityCipher if ossl_name == "none"
|
|
49
72
|
|
|
@@ -53,6 +76,7 @@ module Net
|
|
|
53
76
|
|
|
54
77
|
cipher.padding = 0
|
|
55
78
|
|
|
79
|
+
cipher.extend(Net::SSH::Transport::OpenSSLCipherExtensions)
|
|
56
80
|
if name =~ /-ctr(@openssh.org)?$/
|
|
57
81
|
if ossl_name !~ /-ctr/
|
|
58
82
|
cipher.extend(Net::SSH::Transport::CTR)
|
|
@@ -75,6 +99,9 @@ module Net
|
|
|
75
99
|
# of the tuple.
|
|
76
100
|
# if :iv_len option is supplied the third return value will be ivlen
|
|
77
101
|
def self.get_lengths(name, options = {})
|
|
102
|
+
klass = SSH_TO_CLASS[name]
|
|
103
|
+
return [klass.key_length, klass.block_size] unless klass.nil?
|
|
104
|
+
|
|
78
105
|
ossl_name = SSH_TO_OSSL[name]
|
|
79
106
|
if ossl_name.nil? || ossl_name == "none"
|
|
80
107
|
result = [0, 0]
|
|
@@ -0,0 +1,207 @@
|
|
|
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
|
+
# rubocop:disable Metrics/AbcSize
|
|
9
|
+
def self.extended(orig)
|
|
10
|
+
# rubocop:disable Metrics/BlockLength
|
|
11
|
+
orig.class_eval do
|
|
12
|
+
include Net::SSH::Loggable
|
|
13
|
+
|
|
14
|
+
attr_reader :cipher
|
|
15
|
+
attr_reader :key
|
|
16
|
+
attr_accessor :nonce
|
|
17
|
+
|
|
18
|
+
#
|
|
19
|
+
# Semantically gcm cipher supplies the OpenSSL iv interface with a nonce
|
|
20
|
+
# as it is not randomly generated due to being supplied from a counter.
|
|
21
|
+
# The RFC's use IV and nonce interchangeably.
|
|
22
|
+
#
|
|
23
|
+
def initialize(encrypt:, key:)
|
|
24
|
+
@cipher = OpenSSL::Cipher.new(algo_name)
|
|
25
|
+
@key = key
|
|
26
|
+
key_len = @cipher.key_len
|
|
27
|
+
if key.size != key_len
|
|
28
|
+
error_message = "#{cipher_name}: keylength does not match"
|
|
29
|
+
error { error_message }
|
|
30
|
+
raise error_message
|
|
31
|
+
end
|
|
32
|
+
encrypt ? @cipher.encrypt : @cipher.decrypt
|
|
33
|
+
@cipher.key = key
|
|
34
|
+
|
|
35
|
+
@nonce = {
|
|
36
|
+
fixed: nil,
|
|
37
|
+
invocation_counter: 0
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def update_cipher_mac(payload, _sequence_number)
|
|
42
|
+
#
|
|
43
|
+
# --- RFC 5647 7.3 ---
|
|
44
|
+
# When using AES-GCM with secure shell, the packet_length field is to
|
|
45
|
+
# be treated as additional authenticated data, not as plaintext.
|
|
46
|
+
#
|
|
47
|
+
length_data = [payload.bytesize].pack('N')
|
|
48
|
+
|
|
49
|
+
cipher.auth_data = length_data
|
|
50
|
+
|
|
51
|
+
encrypted_data = cipher.update(payload) << cipher.final
|
|
52
|
+
|
|
53
|
+
mac = cipher.auth_tag
|
|
54
|
+
|
|
55
|
+
incr_nonce
|
|
56
|
+
length_data + encrypted_data + mac
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
#
|
|
60
|
+
# --- RFC 5647 ---
|
|
61
|
+
# uint32 packet_length; // 0 <= packet_length < 2^32
|
|
62
|
+
#
|
|
63
|
+
def read_length(data, _sequence_number)
|
|
64
|
+
data.unpack1('N')
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#
|
|
68
|
+
# --- RFC 5647 ---
|
|
69
|
+
# In AES-GCM secure shell, the inputs to the authenticated encryption
|
|
70
|
+
# are:
|
|
71
|
+
# PT (Plain Text)
|
|
72
|
+
# byte padding_length; // 4 <= padding_length < 256
|
|
73
|
+
# byte[n1] payload; // n1 = packet_length-padding_length-1
|
|
74
|
+
# byte[n2] random_padding; // n2 = padding_length
|
|
75
|
+
# AAD (Additional Authenticated Data)
|
|
76
|
+
# uint32 packet_length; // 0 <= packet_length < 2^32
|
|
77
|
+
# IV (Initialization Vector)
|
|
78
|
+
# As described in section 7.1.
|
|
79
|
+
# BK (Block Cipher Key)
|
|
80
|
+
# The appropriate Encryption Key formed during the Key Exchange.
|
|
81
|
+
#
|
|
82
|
+
def read_and_mac(data, mac, _sequence_number)
|
|
83
|
+
# The authentication tag will be placed in the MAC field at the end of the packet
|
|
84
|
+
|
|
85
|
+
# OpenSSL does not verify auth tag length
|
|
86
|
+
# GCM mode allows arbitrary sizes for the auth_tag up to 128 bytes and a single
|
|
87
|
+
# byte allows authentication to pass. If single byte auth tags are possible
|
|
88
|
+
# an attacker would require no more than 256 attempts to forge a valid tag.
|
|
89
|
+
#
|
|
90
|
+
raise 'incorrect auth_tag length' unless mac.to_s.length == mac_length
|
|
91
|
+
|
|
92
|
+
packet_length = data.unpack1('N')
|
|
93
|
+
|
|
94
|
+
cipher.auth_tag = mac.to_s
|
|
95
|
+
cipher.auth_data = [packet_length].pack('N')
|
|
96
|
+
|
|
97
|
+
result = cipher.update(data[4...]) << cipher.final
|
|
98
|
+
incr_nonce
|
|
99
|
+
result
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def mac_length
|
|
103
|
+
16
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def block_size
|
|
107
|
+
16
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def self.block_size
|
|
111
|
+
16
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
#
|
|
115
|
+
# --- RFC 5647 ---
|
|
116
|
+
# N_MIN minimum nonce (IV) length 12 octets
|
|
117
|
+
# N_MAX maximum nonce (IV) length 12 octets
|
|
118
|
+
#
|
|
119
|
+
def iv_len
|
|
120
|
+
12
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
#
|
|
124
|
+
# --- RFC 5288 ---
|
|
125
|
+
# Each value of the nonce_explicit MUST be distinct for each distinct
|
|
126
|
+
# invocation of the GCM encrypt function for any fixed key. Failure to
|
|
127
|
+
# meet this uniqueness requirement can significantly degrade security.
|
|
128
|
+
# The nonce_explicit MAY be the 64-bit sequence number.
|
|
129
|
+
#
|
|
130
|
+
# --- RFC 5116 ---
|
|
131
|
+
# (2.1) Applications that can generate distinct nonces SHOULD use the nonce
|
|
132
|
+
# formation method defined in Section 3.2, and MAY use any
|
|
133
|
+
# other method that meets the uniqueness requirement.
|
|
134
|
+
#
|
|
135
|
+
# (3.2) The following method to construct nonces is RECOMMENDED.
|
|
136
|
+
#
|
|
137
|
+
# <- variable -> <- variable ->
|
|
138
|
+
# - - - - - - - - - - - - - -
|
|
139
|
+
# | fixed | counter |
|
|
140
|
+
#
|
|
141
|
+
# Initial octets consist of a fixed field and final octets consist of a
|
|
142
|
+
# Counter field. Implementations SHOULD support 12-octet nonces in which
|
|
143
|
+
# the Counter field is four octets long.
|
|
144
|
+
# The Counter fields of successive nonces form a monotonically increasing
|
|
145
|
+
# sequence, when those fields are regarded as unsignd integers in network
|
|
146
|
+
# byte order.
|
|
147
|
+
# The Counter part SHOULD be equal to zero for the first nonce and increment
|
|
148
|
+
# by one for each successive nonce that is generated.
|
|
149
|
+
# The Fixed field MUST remain constant for all nonces that are generated for
|
|
150
|
+
# a given encryption device.
|
|
151
|
+
#
|
|
152
|
+
# --- RFC 5647 ---
|
|
153
|
+
# The invocation field is treated as a 64-bit integer and is increment after
|
|
154
|
+
# each invocation of AES-GCM to process a binary packet.
|
|
155
|
+
# AES-GCM produces a keystream in blocks of 16-octets that is used to
|
|
156
|
+
# encrypt the plaintext. This keystream is produced by encrypting the
|
|
157
|
+
# following 16-octet data structure:
|
|
158
|
+
#
|
|
159
|
+
# uint32 fixed; // 4 octets
|
|
160
|
+
# uint64 invocation_counter; // 8 octets
|
|
161
|
+
# unit32 block_counter; // 4 octets
|
|
162
|
+
#
|
|
163
|
+
# The block_counter is initially set to one (1) and increment as each block
|
|
164
|
+
# of key is produced.
|
|
165
|
+
#
|
|
166
|
+
# The reader is reminded that SSH requires that the data to be encrypted
|
|
167
|
+
# MUST be padded out to a multiple of the block size (16-octets for AES-GCM).
|
|
168
|
+
#
|
|
169
|
+
def incr_nonce
|
|
170
|
+
return if nonce[:fixed].nil?
|
|
171
|
+
|
|
172
|
+
nonce[:invocation_counter] = [nonce[:invocation_counter].to_s.unpack1('B*').to_i(2) + 1].pack('Q>*')
|
|
173
|
+
|
|
174
|
+
apply_nonce
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def nonce=(iv_s)
|
|
178
|
+
return if nonce[:fixed]
|
|
179
|
+
|
|
180
|
+
nonce[:fixed] = iv_s[0...4]
|
|
181
|
+
nonce[:invocation_counter] = iv_s[4...12]
|
|
182
|
+
|
|
183
|
+
apply_nonce
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def apply_nonce
|
|
187
|
+
cipher.iv = "#{nonce[:fixed]}#{nonce[:invocation_counter]}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
#
|
|
191
|
+
# --- RFC 5647 ---
|
|
192
|
+
# If AES-GCM is selected as the encryption algorithm for a given
|
|
193
|
+
# tunnel, AES-GCM MUST also be selected as the Message Authentication
|
|
194
|
+
# Code (MAC) algorithm. Conversely, if AES-GCM is selected as the MAC
|
|
195
|
+
# algorithm, it MUST also be selected as the encryption algorithm.
|
|
196
|
+
#
|
|
197
|
+
def implicit_mac?
|
|
198
|
+
true
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
# rubocop:enable Metrics/BlockLength
|
|
203
|
+
end
|
|
204
|
+
# rubocop:enable Metrics/AbcSize
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
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
|
|
@@ -11,6 +11,10 @@ module Net
|
|
|
11
11
|
8
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
+
def key_length
|
|
15
|
+
0
|
|
16
|
+
end
|
|
17
|
+
|
|
14
18
|
# Returns an arbitrary integer.
|
|
15
19
|
def iv_len
|
|
16
20
|
4
|
|
@@ -50,6 +54,10 @@ module Net
|
|
|
50
54
|
def reset
|
|
51
55
|
self
|
|
52
56
|
end
|
|
57
|
+
|
|
58
|
+
def implicit_mac?
|
|
59
|
+
false
|
|
60
|
+
end
|
|
53
61
|
end
|
|
54
62
|
end
|
|
55
63
|
end
|
|
@@ -12,7 +12,7 @@ module Net
|
|
|
12
12
|
# module. It adds SSH encryption, compression, and packet validation, as
|
|
13
13
|
# per the SSH2 protocol. It also adds an abstraction for polling packets,
|
|
14
14
|
# to allow for both blocking and non-blocking reads.
|
|
15
|
-
module PacketStream
|
|
15
|
+
module PacketStream # rubocop:disable Metrics/ModuleLength
|
|
16
16
|
PROXY_COMMAND_HOST_IP = '<no hostip for proxy command>'.freeze
|
|
17
17
|
|
|
18
18
|
include BufferedIo
|
|
@@ -123,12 +123,12 @@ module Net
|
|
|
123
123
|
# Enqueues a packet to be sent, but does not immediately send the packet.
|
|
124
124
|
# The given payload is pre-processed according to the algorithms specified
|
|
125
125
|
# in the client state (compression, cipher, and hmac).
|
|
126
|
-
def enqueue_packet(payload)
|
|
126
|
+
def enqueue_packet(payload) # rubocop:disable Metrics/AbcSize
|
|
127
127
|
# try to compress the packet
|
|
128
128
|
payload = client.compress(payload)
|
|
129
129
|
|
|
130
130
|
# the length of the packet, minus the padding
|
|
131
|
-
actual_length = (client.hmac.etm ? 0 : 4) + payload.bytesize + 1
|
|
131
|
+
actual_length = (client.hmac.etm || client.hmac.aead ? 0 : 4) + payload.bytesize + 1
|
|
132
132
|
|
|
133
133
|
# compute the padding length
|
|
134
134
|
padding_length = client.block_size - (actual_length % client.block_size)
|
|
@@ -144,11 +144,14 @@ module Net
|
|
|
144
144
|
|
|
145
145
|
padding = Array.new(padding_length) { rand(256) }.pack("C*")
|
|
146
146
|
|
|
147
|
-
if client.
|
|
147
|
+
if client.cipher.implicit_mac?
|
|
148
|
+
unencrypted_data = [padding_length, payload, padding].pack("CA*A*")
|
|
149
|
+
message = client.cipher.update_cipher_mac(unencrypted_data, client.sequence_number)
|
|
150
|
+
elsif client.hmac.etm
|
|
148
151
|
debug { "using encrypt-then-mac" }
|
|
149
152
|
|
|
150
153
|
# Encrypt padding_length, payload, and padding. Take MAC
|
|
151
|
-
# from the unencrypted
|
|
154
|
+
# from the unencrypted packet_length and the encrypted
|
|
152
155
|
# data.
|
|
153
156
|
length_data = [packet_length].pack("N")
|
|
154
157
|
|
|
@@ -216,7 +219,7 @@ module Net
|
|
|
216
219
|
# new Packet object.
|
|
217
220
|
# rubocop:disable Metrics/AbcSize
|
|
218
221
|
def poll_next_packet
|
|
219
|
-
aad_length = server.hmac.etm ? 4 : 0
|
|
222
|
+
aad_length = server.hmac.etm || server.hmac.aead ? 4 : 0
|
|
220
223
|
|
|
221
224
|
if @packet.nil?
|
|
222
225
|
minimum = server.block_size < 4 ? 4 : server.block_size
|
|
@@ -225,7 +228,11 @@ module Net
|
|
|
225
228
|
data = read_available(minimum + aad_length)
|
|
226
229
|
|
|
227
230
|
# decipher it
|
|
228
|
-
if server.
|
|
231
|
+
if server.cipher.implicit_mac?
|
|
232
|
+
@packet_length = server.cipher.read_length(data[0...4], server.sequence_number)
|
|
233
|
+
@packet = Net::SSH::Buffer.new
|
|
234
|
+
@mac_data = data
|
|
235
|
+
elsif server.hmac.etm
|
|
229
236
|
@packet_length = data.unpack("N").first
|
|
230
237
|
@mac_data = data
|
|
231
238
|
@packet = Net::SSH::Buffer.new(server.update_cipher(data[aad_length..-1]))
|
|
@@ -238,31 +245,45 @@ module Net
|
|
|
238
245
|
need = @packet_length + 4 - aad_length - server.block_size
|
|
239
246
|
raise Net::SSH::Exception, "padding error, need #{need} block #{server.block_size}" if need % server.block_size != 0
|
|
240
247
|
|
|
241
|
-
|
|
248
|
+
if server.cipher.implicit_mac?
|
|
249
|
+
return nil if available < need + server.cipher.mac_length
|
|
250
|
+
else
|
|
251
|
+
return nil if available < need + server.hmac.mac_length # rubocop:disable Style/IfInsideElse
|
|
252
|
+
end
|
|
242
253
|
|
|
243
254
|
if need > 0
|
|
244
255
|
# read the remainder of the packet and decrypt it.
|
|
245
256
|
data = read_available(need)
|
|
246
|
-
@mac_data += data if server.hmac.etm
|
|
247
|
-
|
|
257
|
+
@mac_data += data if server.hmac.etm || server.cipher.implicit_mac?
|
|
258
|
+
unless server.cipher.implicit_mac?
|
|
259
|
+
@packet.append(
|
|
260
|
+
server.update_cipher(data)
|
|
261
|
+
)
|
|
262
|
+
end
|
|
248
263
|
end
|
|
249
264
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
265
|
+
if server.cipher.implicit_mac?
|
|
266
|
+
real_hmac = read_available(server.cipher.mac_length) || ""
|
|
267
|
+
@packet = Net::SSH::Buffer.new(server.cipher.read_and_mac(@mac_data, real_hmac, server.sequence_number))
|
|
268
|
+
padding_length = @packet.read_byte
|
|
269
|
+
payload = @packet.read(@packet_length - padding_length - 1)
|
|
270
|
+
else
|
|
271
|
+
# get the hmac from the tail of the packet (if one exists), and
|
|
272
|
+
# then validate it.
|
|
273
|
+
real_hmac = read_available(server.hmac.mac_length) || ""
|
|
256
274
|
|
|
257
|
-
|
|
275
|
+
@packet.append(server.final_cipher)
|
|
276
|
+
padding_length = @packet.read_byte
|
|
258
277
|
|
|
259
|
-
|
|
260
|
-
server.hmac.digest([server.sequence_number, @mac_data].pack("NA*"))
|
|
261
|
-
else
|
|
262
|
-
server.hmac.digest([server.sequence_number, @packet.content].pack("NA*"))
|
|
263
|
-
end
|
|
264
|
-
raise Net::SSH::Exception, "corrupted hmac detected #{server.hmac.class}" if real_hmac != my_computed_hmac
|
|
278
|
+
payload = @packet.read(@packet_length - padding_length - 1)
|
|
265
279
|
|
|
280
|
+
my_computed_hmac = if server.hmac.etm
|
|
281
|
+
server.hmac.digest([server.sequence_number, @mac_data].pack("NA*"))
|
|
282
|
+
else
|
|
283
|
+
server.hmac.digest([server.sequence_number, @packet.content].pack("NA*"))
|
|
284
|
+
end
|
|
285
|
+
raise Net::SSH::Exception, "corrupted hmac detected #{server.hmac.class}" if real_hmac != my_computed_hmac
|
|
286
|
+
end
|
|
266
287
|
# try to decompress the payload, in case compression is active
|
|
267
288
|
payload = server.decompress(payload)
|
|
268
289
|
|
|
@@ -125,7 +125,7 @@ module Net
|
|
|
125
125
|
compressor.deflate(data, Zlib::SYNC_FLUSH)
|
|
126
126
|
end
|
|
127
127
|
|
|
128
|
-
#
|
|
128
|
+
# Decompresses the data. If no compression is in effect, this will just return
|
|
129
129
|
# the data unmodified, otherwise it uses #decompressor to decompress the data.
|
|
130
130
|
def decompress(data)
|
|
131
131
|
data = data.to_s
|