net-ssh 7.3.6 → 8.0.2
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/pull_request_template.md +1 -0
- data/.github/workflows/changelog.yml +43 -0
- data/.github/workflows/ci-with-docker.yml +0 -3
- data/.github/workflows/ci.yml +2 -17
- data/.github/workflows/rubocop.yml +3 -1
- data/.rubocop.yml +5 -1
- data/.rubocop_todo.yml +1 -7
- data/CHANGES.txt +82 -7
- data/CONTRIBUTING.md +35 -0
- data/Dockerfile +3 -3
- data/Dockerfile.openssl3 +1 -1
- data/Dockerfile.zlib_ng +1 -1
- data/README.md +71 -57
- data/Rakefile +66 -12
- data/appveyor.yml +4 -16
- data/changelog.d/README.md +1 -0
- data/docker-compose.yml +1 -7
- data/lib/net/ssh/authentication/agent.rb +35 -11
- data/lib/net/ssh/authentication/ed25519.rb +137 -44
- data/lib/net/ssh/authentication/ed25519_loader.rb +14 -9
- data/lib/net/ssh/authentication/key_manager.rb +22 -11
- 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 +28 -12
- data/lib/net/ssh/connection/channel.rb +49 -2
- data/lib/net/ssh/connection/session.rb +58 -8
- data/lib/net/ssh/errors.rb +24 -6
- data/lib/net/ssh/key_factory.rb +7 -1
- data/lib/net/ssh/known_hosts.rb +18 -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/jump.rb +40 -2
- data/lib/net/ssh/service/forward.rb +35 -24
- data/lib/net/ssh/test.rb +2 -1
- data/lib/net/ssh/transport/algorithms.rb +88 -33
- 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/constants.rb +1 -0
- data/lib/net/ssh/transport/gcm_cipher.rb +27 -6
- 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 +12 -0
- data/lib/net/ssh/transport/session.rb +43 -45
- data/lib/net/ssh/verifiers/always.rb +0 -3
- data/lib/net/ssh/version.rb +3 -3
- data/lib/net/ssh.rb +141 -58
- data/net-ssh.gemspec +5 -8
- data/rakelib/changelog.rb +229 -0
- data.tar.gz.sig +0 -0
- metadata +37 -36
- metadata.gz.sig +0 -0
- data/Gemfile.noed25519 +0 -12
- data/Gemfile.norbnacl +0 -12
data/Rakefile
CHANGED
|
@@ -9,7 +9,11 @@ require "rake"
|
|
|
9
9
|
require "rake/clean"
|
|
10
10
|
require "bundler/gem_tasks"
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
begin
|
|
13
|
+
require "rdoc/task"
|
|
14
|
+
rescue LoadError
|
|
15
|
+
# rdoc is no longer a default gem as of Ruby 4.0; the rest of the Rakefile works without it.
|
|
16
|
+
end
|
|
13
17
|
|
|
14
18
|
desc "When releasing make sure NET_SSH_BUILDGEM_SIGNED is set"
|
|
15
19
|
task :check_NET_SSH_BUILDGEM_SIGNED do
|
|
@@ -24,20 +28,23 @@ CLEAN.include ['pkg', 'rdoc']
|
|
|
24
28
|
name = "net-ssh"
|
|
25
29
|
|
|
26
30
|
require_relative "lib/net/ssh/version"
|
|
31
|
+
require_relative "rakelib/changelog"
|
|
27
32
|
version = Net::SSH::Version::CURRENT
|
|
28
33
|
|
|
29
34
|
extra_files = %w[LICENSE.txt THANKS.txt CHANGES.txt]
|
|
30
|
-
RDoc::Task
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
if defined?(RDoc::Task)
|
|
36
|
+
RDoc::Task.new do |rdoc|
|
|
37
|
+
rdoc.rdoc_dir = "rdoc"
|
|
38
|
+
rdoc.title = "#{name} #{version}"
|
|
39
|
+
rdoc.generator = 'hanna' # gem install hanna-nouveau
|
|
40
|
+
rdoc.main = 'README.md'
|
|
41
|
+
rdoc.rdoc_files.include("README*")
|
|
42
|
+
rdoc.rdoc_files.include("bin/*.rb")
|
|
43
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
|
44
|
+
extra_files.each { |file|
|
|
45
|
+
rdoc.rdoc_files.include(file) if File.exist?(file)
|
|
46
|
+
}
|
|
47
|
+
end
|
|
41
48
|
end
|
|
42
49
|
|
|
43
50
|
namespace :cert do
|
|
@@ -103,6 +110,15 @@ def change_version(&block)
|
|
|
103
110
|
raise ArgumentError, "Cound not find line: TINY = \"#{tiny}\" in #{version_file}" unless found[:tiny] || new_tiny == tiny
|
|
104
111
|
end
|
|
105
112
|
|
|
113
|
+
begin
|
|
114
|
+
heading = Changelog.heading(Net::SSH::Version::MAJOR, Net::SSH::Version::MINOR, new_tiny, new_pre)
|
|
115
|
+
section = Changelog.new(__dir__).release(heading)
|
|
116
|
+
rescue StandardError
|
|
117
|
+
FileUtils.rm_f "#{version_file}.new"
|
|
118
|
+
raise
|
|
119
|
+
end
|
|
120
|
+
puts section ? "Added to CHANGES.txt:\n#{section}" : "Nothing to release for a pre-release, CHANGES.txt unchanged"
|
|
121
|
+
|
|
106
122
|
FileUtils.mv version_file, "#{version_file}.old"
|
|
107
123
|
FileUtils.mv "#{version_file}.new", version_file
|
|
108
124
|
end
|
|
@@ -142,6 +158,44 @@ namespace :vbump do
|
|
|
142
158
|
end
|
|
143
159
|
end
|
|
144
160
|
|
|
161
|
+
namespace :changelog do
|
|
162
|
+
changelog = Changelog.new(__dir__)
|
|
163
|
+
|
|
164
|
+
desc "Validate the fragments in changelog.d/"
|
|
165
|
+
task :check do
|
|
166
|
+
count = changelog.check.size
|
|
167
|
+
puts "#{count} valid changelog fragment#{'s' unless count == 1}"
|
|
168
|
+
rescue Changelog::Error => e
|
|
169
|
+
abort e.message
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
desc "Print the CHANGES.txt section the next version bump would write"
|
|
173
|
+
task :preview, [:heading] do |_t, args|
|
|
174
|
+
puts changelog.section(args[:heading] || "Unreleased") || "Nothing to release"
|
|
175
|
+
rescue Changelog::Error => e
|
|
176
|
+
abort e.message
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
desc "Create a fragment: rake \"changelog:new[1234,change,Text of the entry]\""
|
|
180
|
+
task :new, [:number, :type] do |_t, args|
|
|
181
|
+
abort "Usage: rake \"changelog:new[<PR number>,<#{Changelog::TYPES.keys.join('|')}>,<text>]\"" unless args[:number] && args[:type]
|
|
182
|
+
# Rake splits arguments on commas and strips the spaces around them, so take the text from the raw task string.
|
|
183
|
+
raw = Rake.application.top_level_tasks.find { |t| t.start_with?("changelog:new[") }
|
|
184
|
+
text = raw&.[](/\A[^\[]*\[[^,]*,[^,]*,\s*(.*)\]\z/m, 1) || args.extras.join(", ")
|
|
185
|
+
puts "Created #{changelog.new_fragment(args[:number], args[:type], text)}"
|
|
186
|
+
rescue Changelog::Error => e
|
|
187
|
+
abort e.message
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
desc "Write the fragments to CHANGES.txt under a heading, e.g. rake \"changelog:release[7.3.4]\" (vbump does this)"
|
|
191
|
+
task :release, [:heading] do |_t, args|
|
|
192
|
+
abort "Usage: rake \"changelog:release[<version heading>]\"" unless args[:heading]
|
|
193
|
+
puts changelog.release(args[:heading]) || "Nothing to release"
|
|
194
|
+
rescue Changelog::Error => e
|
|
195
|
+
abort e.message
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
145
199
|
namespace :rdoc do
|
|
146
200
|
desc "Update gh-pages branch"
|
|
147
201
|
task :publish do
|
data/appveyor.yml
CHANGED
|
@@ -4,16 +4,9 @@ skip_tags: true
|
|
|
4
4
|
|
|
5
5
|
environment:
|
|
6
6
|
matrix:
|
|
7
|
-
- ruby_version: "
|
|
8
|
-
- ruby_version: "
|
|
9
|
-
- ruby_version: "
|
|
10
|
-
- ruby_version: "24-x64"
|
|
11
|
-
- ruby_version: "23"
|
|
12
|
-
- ruby_version: "23-x64"
|
|
13
|
-
|
|
14
|
-
matrix:
|
|
15
|
-
allow_failures:
|
|
16
|
-
- ruby_version: "jruby-9.1.2.0"
|
|
7
|
+
- ruby_version: "31-x64"
|
|
8
|
+
- ruby_version: "30-x64"
|
|
9
|
+
- ruby_version: "27-x64"
|
|
17
10
|
|
|
18
11
|
#init:
|
|
19
12
|
# - ps: iex ((new-object net.webclient).DownloadString('https://raw.githubusercontent.com/appveyor/ci/master/scripts/enable-rdp.ps1'))
|
|
@@ -27,12 +20,8 @@ platform:
|
|
|
27
20
|
|
|
28
21
|
install:
|
|
29
22
|
- SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
|
|
30
|
-
- if "%ruby_version%" == "jruby-9.1.2.0" ( cinst javaruntime -i )
|
|
31
|
-
- if "%ruby_version%" == "jruby-9.1.2.0" ( cinst jruby --version 9.1.2.0 -i --allow-empty-checksums )
|
|
32
|
-
- if "%ruby_version%" == "jruby-9.1.2.0" ( SET "PATH=C:\jruby-9.1.2.0\bin\;%PATH%" )
|
|
33
23
|
- ruby --version
|
|
34
|
-
- gem install bundler --no-document --user-install -v
|
|
35
|
-
- SET BUNDLE_GEMFILE=Gemfile.noed25519
|
|
24
|
+
- gem install bundler --no-document --user-install -v 2.4.22
|
|
36
25
|
- bundle install --retry=3
|
|
37
26
|
- cinst freesshd
|
|
38
27
|
- cinst putty --allow-empty-checksums
|
|
@@ -51,7 +40,6 @@ install:
|
|
|
51
40
|
}
|
|
52
41
|
|
|
53
42
|
test_script:
|
|
54
|
-
- SET BUNDLE_GEMFILE=Gemfile.noed25519
|
|
55
43
|
- SET NET_SSH_RUN_WIN_INTEGRATION_TESTS=YES
|
|
56
44
|
- bundle exec rake test
|
|
57
45
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Changelog fragments: one `<PR number>.<type>.txt` file per user-visible change, assembled into `CHANGES.txt` when the version is bumped. See [CONTRIBUTING.md](../CONTRIBUTING.md#changelog).
|
data/docker-compose.yml
CHANGED
|
@@ -17,6 +17,9 @@ module Net
|
|
|
17
17
|
# An exception for indicating that the SSH agent is not available.
|
|
18
18
|
class AgentNotAvailable < AgentError; end
|
|
19
19
|
|
|
20
|
+
# Raised when the agent closes the connection before replying.
|
|
21
|
+
class AgentClosedConnection < AgentError; end
|
|
22
|
+
|
|
20
23
|
# This class implements a simple client for the ssh-agent protocol. It
|
|
21
24
|
# does not implement any specific protocol, but instead copies the
|
|
22
25
|
# behavior of the ssh-agent functions in the OpenSSH library (3.8).
|
|
@@ -83,12 +86,18 @@ module Net
|
|
|
83
86
|
# socket reports that it is an SSH2-compatible agent, this will fail
|
|
84
87
|
# (it only supports the ssh-agent distributed by OpenSSH).
|
|
85
88
|
def connect!(agent_socket_factory = nil, identity_agent = nil)
|
|
89
|
+
@agent_socket_factory = agent_socket_factory
|
|
90
|
+
@identity_agent = identity_agent
|
|
91
|
+
open_agent_socket!
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def open_agent_socket!
|
|
86
95
|
debug { "connecting to ssh-agent" }
|
|
87
96
|
@socket =
|
|
88
|
-
if agent_socket_factory
|
|
89
|
-
agent_socket_factory.call
|
|
90
|
-
elsif identity_agent
|
|
91
|
-
unix_socket_class.open(File.expand_path(identity_agent))
|
|
97
|
+
if @agent_socket_factory
|
|
98
|
+
@agent_socket_factory.call
|
|
99
|
+
elsif @identity_agent
|
|
100
|
+
unix_socket_class.open(File.expand_path(@identity_agent))
|
|
92
101
|
elsif ENV['SSH_AUTH_SOCK'] && unix_socket_class
|
|
93
102
|
unix_socket_class.open(File.expand_path(ENV['SSH_AUTH_SOCK']))
|
|
94
103
|
elsif Gem.win_platform? && RUBY_ENGINE != "jruby"
|
|
@@ -100,6 +109,7 @@ module Net
|
|
|
100
109
|
error { "could not connect to ssh-agent: #{e.message}" }
|
|
101
110
|
raise AgentNotAvailable, $!.message
|
|
102
111
|
end
|
|
112
|
+
private :open_agent_socket!
|
|
103
113
|
|
|
104
114
|
# Attempts to negotiate the SSH agent protocol version. Raises an error
|
|
105
115
|
# if the version could not be negotiated successfully.
|
|
@@ -114,6 +124,11 @@ module Net
|
|
|
114
124
|
elsif type != SSH_AGENT_RSA_IDENTITIES_ANSWER1 && type != SSH_AGENT_RSA_IDENTITIES_ANSWER2
|
|
115
125
|
raise AgentNotAvailable, "unknown response from agent: #{type}, #{body.to_s.inspect}"
|
|
116
126
|
end
|
|
127
|
+
rescue AgentClosedConnection
|
|
128
|
+
# Agents built on ssh-agent-lib <= 0.6.0 (e.g. older Proton Pass) drop the connection on
|
|
129
|
+
# this legacy request but handle the rest of the protocol (#1008).
|
|
130
|
+
debug { "ssh-agent closed the connection on the version request; assuming a modern agent and reconnecting" }
|
|
131
|
+
open_agent_socket!
|
|
117
132
|
end
|
|
118
133
|
|
|
119
134
|
# Return an array of all identities (public keys) known to the agent.
|
|
@@ -137,7 +152,7 @@ module Net
|
|
|
137
152
|
key.extend(Comment)
|
|
138
153
|
key.comment = comment_str
|
|
139
154
|
identities.push key
|
|
140
|
-
rescue NotImplementedError => e
|
|
155
|
+
rescue Net::SSH::UnsupportedKeyType, NotImplementedError => e
|
|
141
156
|
error { "ignoring unimplemented key:#{e.message} #{comment_str}" }
|
|
142
157
|
end
|
|
143
158
|
end
|
|
@@ -223,8 +238,14 @@ module Net
|
|
|
223
238
|
# tuple consisting of the packet type, and the packet's body (which
|
|
224
239
|
# is returned as a Net::SSH::Buffer).
|
|
225
240
|
def read_packet
|
|
226
|
-
|
|
227
|
-
|
|
241
|
+
header = @socket.read(4)
|
|
242
|
+
raise AgentClosedConnection, "agent closed the connection" if header.nil?
|
|
243
|
+
|
|
244
|
+
buffer = Net::SSH::Buffer.new(header)
|
|
245
|
+
body = @socket.read(buffer.read_long)
|
|
246
|
+
raise AgentClosedConnection, "agent closed the connection" if body.nil?
|
|
247
|
+
|
|
248
|
+
buffer.append(body)
|
|
228
249
|
type = buffer.read_byte
|
|
229
250
|
debug { "received agent packet #{type} len #{buffer.length - 4}" }
|
|
230
251
|
return type, buffer
|
|
@@ -235,6 +256,9 @@ module Net
|
|
|
235
256
|
def send_and_wait(type, *args)
|
|
236
257
|
send_packet(type, *args)
|
|
237
258
|
read_packet
|
|
259
|
+
rescue Errno::ECONNRESET, Errno::EPIPE => e
|
|
260
|
+
# Linux reports an agent that closes with our request unread as a reset, not EOF.
|
|
261
|
+
raise AgentClosedConnection, "agent closed the connection (#{e.class})"
|
|
238
262
|
end
|
|
239
263
|
|
|
240
264
|
# Returns +true+ if the parameter indicates a "failure" response from
|
|
@@ -261,13 +285,13 @@ module Net
|
|
|
261
285
|
when /^ecdsa\-sha2\-(\w*)-cert-v01@openssh\.com$/
|
|
262
286
|
Net::SSH::Buffer.from(:string, priv_key.to_blob, :bignum, priv_key.key.private_key).to_s
|
|
263
287
|
when /^ssh-ed25519$/
|
|
264
|
-
Net::SSH::Buffer.from(:string, priv_key.
|
|
265
|
-
:string, priv_key.
|
|
288
|
+
Net::SSH::Buffer.from(:string, priv_key.public_key_bytes,
|
|
289
|
+
:string, priv_key.private_key_bytes_for_agent).to_s
|
|
266
290
|
when /^ssh-ed25519-cert-v01@openssh\.com$/
|
|
267
291
|
# Unlike the other certificate types, the public key is included after the certifiate.
|
|
268
292
|
Net::SSH::Buffer.from(:string, priv_key.to_blob,
|
|
269
|
-
:string, priv_key.key.
|
|
270
|
-
:string, priv_key.key.
|
|
293
|
+
:string, priv_key.key.public_key_bytes,
|
|
294
|
+
:string, priv_key.key.private_key_bytes_for_agent).to_s
|
|
271
295
|
when /^ssh-rsa$/
|
|
272
296
|
# `n` and `e` are reversed compared to the ordering in `OpenSSL::PKey::RSA#to_blob`.
|
|
273
297
|
Net::SSH::Buffer.from(:bignum, priv_key.n, :bignum, priv_key.e, :bignum, priv_key.d,
|
|
@@ -1,25 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
gem 'bcrypt_pbkdf', '~> 1.0' unless RUBY_PLATFORM == "java"
|
|
3
|
-
|
|
4
|
-
require 'ed25519'
|
|
5
|
-
|
|
1
|
+
require 'openssl'
|
|
6
2
|
require 'net/ssh/transport/cipher_factory'
|
|
7
3
|
require 'net/ssh/authentication/pub_key_fingerprint'
|
|
8
|
-
require 'bcrypt_pbkdf' unless RUBY_PLATFORM == "java"
|
|
9
4
|
|
|
10
5
|
module Net
|
|
11
6
|
module SSH
|
|
12
7
|
module Authentication
|
|
13
8
|
module ED25519
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
raise ArgumentError, "pk does not match sk" unless pk == key.verify_key.to_bytes
|
|
9
|
+
ALGORITHM = "ED25519"
|
|
10
|
+
KEY_BYTES = 32
|
|
11
|
+
PRIVATE_KEY_BYTES = 64
|
|
18
12
|
|
|
19
|
-
|
|
13
|
+
class UnsupportedError < StandardError; end
|
|
14
|
+
|
|
15
|
+
def self.ensure_supported!
|
|
16
|
+
unless OpenSSL::PKey.respond_to?(:new_raw_public_key) && OpenSSL::PKey.respond_to?(:new_raw_private_key)
|
|
17
|
+
raise UnsupportedError, "OpenSSL::PKey raw public/private key APIs are unavailable"
|
|
20
18
|
end
|
|
19
|
+
|
|
20
|
+
key = new_private_key("\x00" * KEY_BYTES)
|
|
21
|
+
signature = sign(key, "")
|
|
22
|
+
public_key = new_public_key(key.raw_public_key)
|
|
23
|
+
raise UnsupportedError, "OpenSSL Ed25519 signature verification failed" unless verify(public_key, signature, "")
|
|
24
|
+
rescue OpenSSL::PKey::PKeyError => e
|
|
25
|
+
raise UnsupportedError, e.message
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.new_public_key(key)
|
|
29
|
+
validate_key_bytes!(key, "public key", KEY_BYTES)
|
|
30
|
+
OpenSSL::PKey.new_raw_public_key(ALGORITHM, binary_string(key))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.new_private_key(key)
|
|
34
|
+
validate_key_bytes!(key, "private key", KEY_BYTES)
|
|
35
|
+
OpenSSL::PKey.new_raw_private_key(ALGORITHM, binary_string(key))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.sign(key, data)
|
|
39
|
+
key.sign(nil, data)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.verify(key, signature, data)
|
|
43
|
+
key.verify(nil, signature, data)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.validate_key_bytes!(key, label, expected_bytes)
|
|
47
|
+
raise ArgumentError, "invalid Ed25519 #{label}" unless key.respond_to?(:bytesize) && key.bytesize == expected_bytes
|
|
21
48
|
end
|
|
22
49
|
|
|
50
|
+
def self.binary_string(string)
|
|
51
|
+
string.dup.force_encoding('BINARY')
|
|
52
|
+
end
|
|
53
|
+
private_class_method :binary_string
|
|
54
|
+
|
|
23
55
|
class OpenSSHPrivateKeyLoader
|
|
24
56
|
CipherFactory = Net::SSH::Transport::CipherFactory
|
|
25
57
|
|
|
@@ -41,13 +73,8 @@ module Net
|
|
|
41
73
|
end
|
|
42
74
|
end
|
|
43
75
|
|
|
44
|
-
def self.check_bcrypt_kdf!(rounds, password)
|
|
45
|
-
raise ArgumentError.new("Private key bcrypt rounds #{rounds} exceeds #{MAX_KDF_ROUNDS}") if rounds > MAX_KDF_ROUNDS
|
|
46
|
-
raise DecryptError.new("Passphrase required for encrypted private key", encrypted_key: true) if password.nil? || password.empty?
|
|
47
|
-
end
|
|
48
|
-
|
|
49
76
|
def self.read(datafull, password)
|
|
50
|
-
datafull = datafull.strip
|
|
77
|
+
datafull = datafull.strip.encode(universal_newline: true)
|
|
51
78
|
raise ArgumentError.new("Expected #{MBEGIN} at start of private key") unless datafull.start_with?(MBEGIN)
|
|
52
79
|
raise ArgumentError.new("Expected #{MEND} at end of private key") unless datafull.end_with?(MEND)
|
|
53
80
|
|
|
@@ -55,7 +82,7 @@ module Net
|
|
|
55
82
|
data = datab64.unpack1("m")
|
|
56
83
|
raise ArgumentError.new("Expected #{MAGIC} at start of decoded private key") unless data.start_with?(MAGIC)
|
|
57
84
|
|
|
58
|
-
buffer = Net::SSH::Buffer.new(data[MAGIC.size + 1..-1])
|
|
85
|
+
buffer = Net::SSH::Buffer.new(data[(MAGIC.size + 1)..-1])
|
|
59
86
|
|
|
60
87
|
ciphername = buffer.read_string
|
|
61
88
|
raise ArgumentError.new("#{ciphername} in private key is not supported") unless
|
|
@@ -71,29 +98,35 @@ module Net
|
|
|
71
98
|
_pubkey = buffer.read_string
|
|
72
99
|
|
|
73
100
|
len = buffer.read_long
|
|
101
|
+
encrypted_private = buffer.read(len)
|
|
74
102
|
|
|
75
103
|
keylen, blocksize, ivlen = CipherFactory.get_lengths(ciphername, iv_len: true)
|
|
76
104
|
raise ArgumentError.new("Private key len:#{len} is not a multiple of #{blocksize}") if
|
|
77
|
-
(
|
|
105
|
+
(len < blocksize) || ((blocksize > 0) && (len % blocksize) != 0)
|
|
106
|
+
|
|
107
|
+
raise ArgumentError.new("Private key len:#{len} exceeds available data") unless encrypted_private.bytesize == len
|
|
108
|
+
|
|
109
|
+
authlen = CipherFactory.auth_length(ciphername)
|
|
110
|
+
auth_tag = authlen > 0 ? buffer.read(authlen) : nil
|
|
78
111
|
|
|
79
112
|
if kdfname == 'bcrypt'
|
|
80
113
|
salt = kdfopts.read_string
|
|
81
114
|
rounds = kdfopts.read_long
|
|
115
|
+
raise ArgumentError.new("Private key bcrypt rounds #{rounds} exceeds #{MAX_KDF_ROUNDS}") if rounds > MAX_KDF_ROUNDS
|
|
82
116
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
check_bcrypt_kdf!(rounds, password)
|
|
86
|
-
|
|
87
|
-
key = BCryptPbkdf::key(password, salt, keylen + ivlen, rounds)
|
|
88
|
-
raise DecryptError.new("BCyryptPbkdf failed", encrypted_key: true) unless key
|
|
117
|
+
key = bcrypt_pbkdf_key(password, salt, keylen + ivlen, rounds)
|
|
89
118
|
else
|
|
90
|
-
key =
|
|
119
|
+
key = "\x00" * (keylen + ivlen)
|
|
91
120
|
end
|
|
92
121
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
122
|
+
decoded = decrypt_private_key(
|
|
123
|
+
ciphername,
|
|
124
|
+
encrypted_private,
|
|
125
|
+
auth_tag,
|
|
126
|
+
key: key[0...keylen],
|
|
127
|
+
iv: key[keylen...(keylen + ivlen)],
|
|
128
|
+
encrypted_key: kdfname == 'bcrypt'
|
|
129
|
+
)
|
|
97
130
|
|
|
98
131
|
decoded = Net::SSH::Buffer.new(decoded)
|
|
99
132
|
check1 = decoded.read_long
|
|
@@ -109,15 +142,50 @@ module Net
|
|
|
109
142
|
decoded.read_private_keyblob(type_name)
|
|
110
143
|
end
|
|
111
144
|
end
|
|
145
|
+
|
|
146
|
+
def self.bcrypt_pbkdf_key(password, salt, length, rounds)
|
|
147
|
+
begin
|
|
148
|
+
require_bcrypt_pbkdf
|
|
149
|
+
rescue LoadError
|
|
150
|
+
raise DecryptError.new("bcrypt_pbkdf is required to decrypt bcrypt-encrypted OpenSSH private keys")
|
|
151
|
+
end
|
|
152
|
+
raise DecryptError.new("Passphrase required for encrypted private key", encrypted_key: true) if password.nil? || password.empty?
|
|
153
|
+
|
|
154
|
+
key = BCryptPbkdf::key(password, salt, length, rounds)
|
|
155
|
+
raise DecryptError.new("BCryptPbkdf failed", encrypted_key: true) unless key
|
|
156
|
+
|
|
157
|
+
key
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def self.require_bcrypt_pbkdf
|
|
161
|
+
require 'bcrypt_pbkdf'
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.decrypt_private_key(ciphername, encrypted_private, auth_tag, options)
|
|
165
|
+
if auth_tag
|
|
166
|
+
return CipherFactory.decrypt_private_key(
|
|
167
|
+
ciphername,
|
|
168
|
+
encrypted_private,
|
|
169
|
+
auth_tag,
|
|
170
|
+
options[:key],
|
|
171
|
+
options[:iv]
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
cipher = CipherFactory.get(ciphername, key: options[:key], iv: options[:iv], decrypt: true)
|
|
176
|
+
decoded = cipher.update(encrypted_private)
|
|
177
|
+
decoded << cipher.final
|
|
178
|
+
rescue Net::SSH::Exception, OpenSSL::Cipher::CipherError
|
|
179
|
+
raise DecryptError.new("Decrypt failed on private key", encrypted_key: options[:encrypted_key])
|
|
180
|
+
end
|
|
112
181
|
end
|
|
113
182
|
|
|
114
183
|
class PubKey
|
|
115
184
|
include Net::SSH::Authentication::PubKeyFingerprint
|
|
116
185
|
|
|
117
|
-
attr_reader :verify_key
|
|
118
|
-
|
|
119
186
|
def initialize(data)
|
|
120
|
-
|
|
187
|
+
ED25519.validate_key_bytes!(data, "public key", KEY_BYTES)
|
|
188
|
+
@public_key_bytes = data.dup.force_encoding('BINARY')
|
|
121
189
|
end
|
|
122
190
|
|
|
123
191
|
def self.read_keyblob(buffer)
|
|
@@ -125,7 +193,7 @@ module Net
|
|
|
125
193
|
end
|
|
126
194
|
|
|
127
195
|
def to_blob
|
|
128
|
-
Net::SSH::Buffer.from(:mstring, "ssh-ed25519".dup, :string,
|
|
196
|
+
Net::SSH::Buffer.from(:mstring, "ssh-ed25519".dup, :string, public_key_bytes).to_s
|
|
129
197
|
end
|
|
130
198
|
|
|
131
199
|
def ssh_type
|
|
@@ -137,22 +205,26 @@ module Net
|
|
|
137
205
|
end
|
|
138
206
|
|
|
139
207
|
def ssh_do_verify(sig, data, options = {})
|
|
140
|
-
|
|
208
|
+
ED25519.verify(verify_key, sig, data)
|
|
141
209
|
end
|
|
142
210
|
|
|
143
211
|
def to_pem
|
|
144
212
|
# TODO this is not pem
|
|
145
|
-
ssh_type + [
|
|
213
|
+
ssh_type + [public_key_bytes].pack("m")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def public_key_bytes
|
|
217
|
+
@public_key_bytes.dup
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def verify_key
|
|
221
|
+
@verify_key ||= ED25519.new_public_key(@public_key_bytes)
|
|
146
222
|
end
|
|
147
223
|
end
|
|
148
224
|
|
|
149
225
|
class PrivKey
|
|
150
226
|
CipherFactory = Net::SSH::Transport::CipherFactory
|
|
151
227
|
|
|
152
|
-
MBEGIN = "-----BEGIN OPENSSH PRIVATE KEY-----\n"
|
|
153
|
-
MEND = "-----END OPENSSH PRIVATE KEY-----\n"
|
|
154
|
-
MAGIC = "openssh-key-v1"
|
|
155
|
-
|
|
156
228
|
attr_reader :sign_key
|
|
157
229
|
|
|
158
230
|
def initialize(buffer)
|
|
@@ -160,8 +232,17 @@ module Net
|
|
|
160
232
|
sk = buffer.read_string
|
|
161
233
|
_comment = buffer.read_string
|
|
162
234
|
|
|
163
|
-
|
|
164
|
-
|
|
235
|
+
ED25519.validate_key_bytes!(pk, "public key", KEY_BYTES)
|
|
236
|
+
ED25519.validate_key_bytes!(sk, "private key field", PRIVATE_KEY_BYTES)
|
|
237
|
+
|
|
238
|
+
private_key_bytes = sk[0, KEY_BYTES]
|
|
239
|
+
raise ArgumentError, "Ed25519 private key does not include matching public key" unless sk[KEY_BYTES, KEY_BYTES] == pk
|
|
240
|
+
|
|
241
|
+
@sign_key = ED25519.new_private_key(private_key_bytes)
|
|
242
|
+
raise ArgumentError, "Ed25519 public key does not match private key" unless @sign_key.raw_public_key == pk
|
|
243
|
+
|
|
244
|
+
@public_key_bytes = pk.dup.force_encoding('BINARY')
|
|
245
|
+
@private_key_bytes = private_key_bytes.dup.force_encoding('BINARY')
|
|
165
246
|
end
|
|
166
247
|
|
|
167
248
|
def to_blob
|
|
@@ -177,11 +258,23 @@ module Net
|
|
|
177
258
|
end
|
|
178
259
|
|
|
179
260
|
def public_key
|
|
180
|
-
PubKey.new(
|
|
261
|
+
PubKey.new(public_key_bytes)
|
|
181
262
|
end
|
|
182
263
|
|
|
183
264
|
def ssh_do_sign(data, sig_alg = nil)
|
|
184
|
-
|
|
265
|
+
ED25519.sign(@sign_key, data)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def public_key_bytes
|
|
269
|
+
@public_key_bytes.dup
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def private_key_bytes
|
|
273
|
+
@private_key_bytes.dup
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def private_key_bytes_for_agent
|
|
277
|
+
private_key_bytes + public_key_bytes
|
|
185
278
|
end
|
|
186
279
|
|
|
187
280
|
def self.read(data, password)
|
|
@@ -1,29 +1,34 @@
|
|
|
1
1
|
module Net
|
|
2
2
|
module SSH
|
|
3
3
|
module Authentication
|
|
4
|
-
# Loads ED25519 support
|
|
5
|
-
# ed25519, bcrypt_pbkdf
|
|
4
|
+
# Loads ED25519 support backed by Ruby OpenSSL raw key APIs.
|
|
6
5
|
module ED25519Loader
|
|
7
6
|
begin
|
|
8
7
|
require 'net/ssh/authentication/ed25519'
|
|
8
|
+
Net::SSH::Authentication::ED25519.ensure_supported!
|
|
9
9
|
LOADED = true
|
|
10
10
|
ERROR = nil
|
|
11
11
|
rescue LoadError => e
|
|
12
12
|
ERROR = e
|
|
13
13
|
LOADED = false
|
|
14
|
+
rescue StandardError => e
|
|
15
|
+
if defined?(Net::SSH::Authentication::ED25519::UnsupportedError) &&
|
|
16
|
+
e.is_a?(Net::SSH::Authentication::ED25519::UnsupportedError)
|
|
17
|
+
ERROR = e
|
|
18
|
+
LOADED = false
|
|
19
|
+
else
|
|
20
|
+
raise
|
|
21
|
+
end
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
def self.raiseUnlessLoaded(message)
|
|
17
|
-
description =
|
|
18
|
-
description
|
|
25
|
+
description = LOADED ? '' : ed25519SupportRequired
|
|
26
|
+
description += "#{ERROR.class} : \"#{ERROR.message}\"\n" if ERROR
|
|
19
27
|
raise NotImplementedError, "#{message}\n#{description}" unless LOADED
|
|
20
28
|
end
|
|
21
29
|
|
|
22
|
-
def self.
|
|
23
|
-
|
|
24
|
-
result << " * ed25519 (>= 1.2, < 2.0)\n"
|
|
25
|
-
result << " * bcrypt_pbkdf (>= 1.0, < 2.0)\n" unless RUBY_PLATFORM == "java"
|
|
26
|
-
result << "See https://github.com/net-ssh/net-ssh/issues/565 for more information\n"
|
|
30
|
+
def self.ed25519SupportRequired
|
|
31
|
+
"net-ssh requires openssl >= 3.2.0 and a crypto backend with Ed25519 enabled for ed25519 support.\n"
|
|
27
32
|
end
|
|
28
33
|
end
|
|
29
34
|
end
|
|
@@ -124,17 +124,16 @@ module Net
|
|
|
124
124
|
|
|
125
125
|
user_identities = load_identities(prepared_identities, false, true)
|
|
126
126
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
end
|
|
127
|
+
agent_identities.each do |key|
|
|
128
|
+
corresponding_user_identity = user_identities.detect { |identity|
|
|
129
|
+
identity[:public_key] && identity[:public_key].to_blob == key.to_blob
|
|
130
|
+
}
|
|
131
|
+
# By identity: EC::Point#== raises TypeError when compared with other key types.
|
|
132
|
+
user_identities.reject! { |identity| identity.equal?(corresponding_user_identity) }
|
|
133
|
+
|
|
134
|
+
if !options[:keys_only] || corresponding_user_identity
|
|
135
|
+
known_identities[key] = { from: :agent, identity: key }
|
|
136
|
+
yield key
|
|
138
137
|
end
|
|
139
138
|
end
|
|
140
139
|
|
|
@@ -246,6 +245,18 @@ module Net
|
|
|
246
245
|
|
|
247
246
|
private
|
|
248
247
|
|
|
248
|
+
# Like OpenSSH's pubkey_prepare, an agent that fails to list its keys is
|
|
249
|
+
# dropped for the session so that key files are still tried.
|
|
250
|
+
def agent_identities
|
|
251
|
+
return [] unless agent
|
|
252
|
+
|
|
253
|
+
agent.identities
|
|
254
|
+
rescue AgentError => e
|
|
255
|
+
error { "could not get identities from agent: #{e.class} (#{e.message})" }
|
|
256
|
+
self.use_agent = false
|
|
257
|
+
[]
|
|
258
|
+
end
|
|
259
|
+
|
|
249
260
|
# Load keycerts from files and data.
|
|
250
261
|
def keycerts
|
|
251
262
|
keycert_files.map { |keycert_file| KeyFactory.load_public_key(keycert_file) } +
|
|
@@ -20,9 +20,7 @@ module Net
|
|
|
20
20
|
# this.
|
|
21
21
|
attr_reader :key_manager
|
|
22
22
|
|
|
23
|
-
#
|
|
24
|
-
# extended to other keys, e.g after reading of
|
|
25
|
-
# PubkeyAcceptedAlgorithms option from ssh_config file is implemented.
|
|
23
|
+
# public key algorithms to be used
|
|
26
24
|
attr_reader :pubkey_algorithms
|
|
27
25
|
|
|
28
26
|
# Instantiates a new authentication method.
|
|
@@ -31,11 +29,7 @@ module Net
|
|
|
31
29
|
@key_manager = options[:key_manager]
|
|
32
30
|
@options = options
|
|
33
31
|
@prompt = options[:password_prompt]
|
|
34
|
-
@pubkey_algorithms =
|
|
35
|
-
|| %w[rsa-sha2-256-cert-v01@openssh.com
|
|
36
|
-
ssh-rsa-cert-v01@openssh.com
|
|
37
|
-
rsa-sha2-256
|
|
38
|
-
ssh-rsa]
|
|
32
|
+
@pubkey_algorithms = session.transport.pubkey_algorithms
|
|
39
33
|
self.logger = session.logger
|
|
40
34
|
end
|
|
41
35
|
|