net-ssh-net-ssh 2.0.12
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.
- data/CHANGELOG.rdoc +137 -0
- data/Manifest +104 -0
- data/README.rdoc +110 -0
- data/Rakefile +79 -0
- data/THANKS.rdoc +16 -0
- data/lib/net/ssh.rb +215 -0
- data/lib/net/ssh/authentication/agent.rb +176 -0
- data/lib/net/ssh/authentication/constants.rb +18 -0
- data/lib/net/ssh/authentication/key_manager.rb +193 -0
- data/lib/net/ssh/authentication/methods/abstract.rb +60 -0
- data/lib/net/ssh/authentication/methods/hostbased.rb +71 -0
- data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +66 -0
- data/lib/net/ssh/authentication/methods/password.rb +39 -0
- data/lib/net/ssh/authentication/methods/publickey.rb +92 -0
- data/lib/net/ssh/authentication/pageant.rb +183 -0
- data/lib/net/ssh/authentication/session.rb +134 -0
- data/lib/net/ssh/buffer.rb +340 -0
- data/lib/net/ssh/buffered_io.rb +149 -0
- data/lib/net/ssh/config.rb +181 -0
- data/lib/net/ssh/connection/channel.rb +625 -0
- data/lib/net/ssh/connection/constants.rb +33 -0
- data/lib/net/ssh/connection/session.rb +596 -0
- data/lib/net/ssh/connection/term.rb +178 -0
- data/lib/net/ssh/errors.rb +85 -0
- data/lib/net/ssh/key_factory.rb +102 -0
- data/lib/net/ssh/known_hosts.rb +129 -0
- data/lib/net/ssh/loggable.rb +61 -0
- data/lib/net/ssh/packet.rb +102 -0
- data/lib/net/ssh/prompt.rb +93 -0
- data/lib/net/ssh/proxy/errors.rb +14 -0
- data/lib/net/ssh/proxy/http.rb +94 -0
- data/lib/net/ssh/proxy/socks4.rb +70 -0
- data/lib/net/ssh/proxy/socks5.rb +129 -0
- data/lib/net/ssh/ruby_compat.rb +7 -0
- data/lib/net/ssh/service/forward.rb +267 -0
- data/lib/net/ssh/test.rb +89 -0
- data/lib/net/ssh/test/channel.rb +129 -0
- data/lib/net/ssh/test/extensions.rb +152 -0
- data/lib/net/ssh/test/kex.rb +44 -0
- data/lib/net/ssh/test/local_packet.rb +51 -0
- data/lib/net/ssh/test/packet.rb +81 -0
- data/lib/net/ssh/test/remote_packet.rb +38 -0
- data/lib/net/ssh/test/script.rb +157 -0
- data/lib/net/ssh/test/socket.rb +59 -0
- data/lib/net/ssh/transport/algorithms.rb +384 -0
- data/lib/net/ssh/transport/cipher_factory.rb +84 -0
- data/lib/net/ssh/transport/constants.rb +30 -0
- data/lib/net/ssh/transport/hmac.rb +31 -0
- data/lib/net/ssh/transport/hmac/abstract.rb +78 -0
- data/lib/net/ssh/transport/hmac/md5.rb +12 -0
- data/lib/net/ssh/transport/hmac/md5_96.rb +11 -0
- data/lib/net/ssh/transport/hmac/none.rb +15 -0
- data/lib/net/ssh/transport/hmac/sha1.rb +13 -0
- data/lib/net/ssh/transport/hmac/sha1_96.rb +11 -0
- data/lib/net/ssh/transport/identity_cipher.rb +55 -0
- data/lib/net/ssh/transport/kex.rb +13 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +208 -0
- data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +77 -0
- data/lib/net/ssh/transport/openssl.rb +128 -0
- data/lib/net/ssh/transport/packet_stream.rb +230 -0
- data/lib/net/ssh/transport/server_version.rb +69 -0
- data/lib/net/ssh/transport/session.rb +276 -0
- data/lib/net/ssh/transport/state.rb +206 -0
- data/lib/net/ssh/verifiers/lenient.rb +30 -0
- data/lib/net/ssh/verifiers/null.rb +12 -0
- data/lib/net/ssh/verifiers/strict.rb +53 -0
- data/lib/net/ssh/version.rb +62 -0
- data/net-ssh.gemspec +128 -0
- data/setup.rb +1585 -0
- data/test/authentication/methods/common.rb +28 -0
- data/test/authentication/methods/test_abstract.rb +51 -0
- data/test/authentication/methods/test_hostbased.rb +114 -0
- data/test/authentication/methods/test_keyboard_interactive.rb +98 -0
- data/test/authentication/methods/test_password.rb +50 -0
- data/test/authentication/methods/test_publickey.rb +127 -0
- data/test/authentication/test_agent.rb +205 -0
- data/test/authentication/test_key_manager.rb +105 -0
- data/test/authentication/test_session.rb +93 -0
- data/test/common.rb +106 -0
- data/test/configs/eqsign +3 -0
- data/test/configs/exact_match +8 -0
- data/test/configs/wild_cards +14 -0
- data/test/connection/test_channel.rb +452 -0
- data/test/connection/test_session.rb +488 -0
- data/test/test_all.rb +6 -0
- data/test/test_buffer.rb +336 -0
- data/test/test_buffered_io.rb +63 -0
- data/test/test_config.rb +84 -0
- data/test/test_key_factory.rb +67 -0
- data/test/transport/hmac/test_md5.rb +39 -0
- data/test/transport/hmac/test_md5_96.rb +25 -0
- data/test/transport/hmac/test_none.rb +34 -0
- data/test/transport/hmac/test_sha1.rb +34 -0
- data/test/transport/hmac/test_sha1_96.rb +25 -0
- data/test/transport/kex/test_diffie_hellman_group1_sha1.rb +146 -0
- data/test/transport/kex/test_diffie_hellman_group_exchange_sha1.rb +92 -0
- data/test/transport/test_algorithms.rb +302 -0
- data/test/transport/test_cipher_factory.rb +171 -0
- data/test/transport/test_hmac.rb +34 -0
- data/test/transport/test_identity_cipher.rb +40 -0
- data/test/transport/test_packet_stream.rb +435 -0
- data/test/transport/test_server_version.rb +68 -0
- data/test/transport/test_session.rb +315 -0
- data/test/transport/test_state.rb +173 -0
- metadata +162 -0
data/lib/net/ssh.rb
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
# Make sure HOME is set, regardless of OS, so that File.expand_path works
|
2
|
+
# as expected with tilde characters.
|
3
|
+
ENV['HOME'] ||= ENV['HOMEPATH'] ? "#{ENV['HOMEDRIVE']}#{ENV['HOMEPATH']}" : "."
|
4
|
+
|
5
|
+
require 'logger'
|
6
|
+
|
7
|
+
require 'net/ssh/config'
|
8
|
+
require 'net/ssh/errors'
|
9
|
+
require 'net/ssh/loggable'
|
10
|
+
require 'net/ssh/transport/session'
|
11
|
+
require 'net/ssh/authentication/session'
|
12
|
+
require 'net/ssh/connection/session'
|
13
|
+
|
14
|
+
module Net
|
15
|
+
|
16
|
+
# Net::SSH is a library for interacting, programmatically, with remote
|
17
|
+
# processes via the SSH2 protocol. Sessions are always initiated via
|
18
|
+
# Net::SSH.start. From there, a program interacts with the new SSH session
|
19
|
+
# via the convenience methods on Net::SSH::Connection::Session, by opening
|
20
|
+
# and interacting with new channels (Net::SSH::Connection:Session#open_channel
|
21
|
+
# and Net::SSH::Connection::Channel), or by forwarding local and/or
|
22
|
+
# remote ports through the connection (Net::SSH::Service::Forward).
|
23
|
+
#
|
24
|
+
# The SSH protocol is very event-oriented. Requests are sent from the client
|
25
|
+
# to the server, and are answered asynchronously. This gives great flexibility
|
26
|
+
# (since clients can have multiple requests pending at a time), but it also
|
27
|
+
# adds complexity. Net::SSH tries to manage this complexity by providing
|
28
|
+
# some simpler methods of synchronous communication (see Net::SSH::Connection::Session#exec!).
|
29
|
+
#
|
30
|
+
# In general, though, and if you want to do anything more complicated than
|
31
|
+
# simply executing commands and capturing their output, you'll need to use
|
32
|
+
# channels (Net::SSH::Connection::Channel) to build state machines that are
|
33
|
+
# executed while the event loop runs (Net::SSH::Connection::Session#loop).
|
34
|
+
#
|
35
|
+
# Net::SSH::Connection::Session and Net::SSH::Connection::Channel have more
|
36
|
+
# information about this technique.
|
37
|
+
#
|
38
|
+
# = "Um, all I want to do is X, just show me how!"
|
39
|
+
#
|
40
|
+
# == X == "execute a command and capture the output"
|
41
|
+
#
|
42
|
+
# Net::SSH.start("host", "user", :password => "password") do |ssh|
|
43
|
+
# result = ssh.exec!("ls -l")
|
44
|
+
# puts result
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# == X == "forward connections on a local port to a remote host"
|
48
|
+
#
|
49
|
+
# Net::SSH.start("host", "user", :password => "password") do |ssh|
|
50
|
+
# ssh.forward.local(1234, "www.google.com", 80)
|
51
|
+
# ssh.loop { true }
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# == X == "forward connections on a remote port to the local host"
|
55
|
+
#
|
56
|
+
# Net::SSH.start("host", "user", :password => "password") do |ssh|
|
57
|
+
# ssh.forward.remote(80, "www.google.com", 1234)
|
58
|
+
# ssh.loop { true }
|
59
|
+
# end
|
60
|
+
module SSH
|
61
|
+
# This is the set of options that Net::SSH.start recognizes. See
|
62
|
+
# Net::SSH.start for a description of each option.
|
63
|
+
VALID_OPTIONS = [
|
64
|
+
:auth_methods, :compression, :compression_level, :config, :encryption,
|
65
|
+
:forward_agent, :hmac, :host_key, :kex, :keys, :key_data, :languages,
|
66
|
+
:logger, :paranoid, :password, :port, :proxy, :rekey_blocks_limit,
|
67
|
+
:rekey_limit, :rekey_packet_limit, :timeout, :verbose,
|
68
|
+
:global_known_hosts_file, :user_known_hosts_file, :host_key_alias,
|
69
|
+
:host_name, :user, :properties, :passphrase
|
70
|
+
]
|
71
|
+
|
72
|
+
# The standard means of starting a new SSH connection. When used with a
|
73
|
+
# block, the connection will be closed when the block terminates, otherwise
|
74
|
+
# the connection will just be returned. The yielded (or returned) value
|
75
|
+
# will be an instance of Net::SSH::Connection::Session (q.v.). (See also
|
76
|
+
# Net::SSH::Connection::Channel and Net::SSH::Service::Forward.)
|
77
|
+
#
|
78
|
+
# Net::SSH.start("host", "user") do |ssh|
|
79
|
+
# ssh.exec! "cp /some/file /another/location"
|
80
|
+
# hostname = ssh.exec!("hostname")
|
81
|
+
#
|
82
|
+
# ssh.open_channel do |ch|
|
83
|
+
# ch.exec "sudo -p 'sudo password: ' ls" do |ch, success|
|
84
|
+
# abort "could not execute sudo ls" unless success
|
85
|
+
#
|
86
|
+
# ch.on_data do |ch, data|
|
87
|
+
# print data
|
88
|
+
# if data =~ /sudo password: /
|
89
|
+
# ch.send_data("password\n")
|
90
|
+
# end
|
91
|
+
# end
|
92
|
+
# end
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# ssh.loop
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# This method accepts the following options (all are optional):
|
99
|
+
#
|
100
|
+
# * :auth_methods => an array of authentication methods to try
|
101
|
+
# * :compression => the compression algorithm to use, or +true+ to use
|
102
|
+
# whatever is supported.
|
103
|
+
# * :compression_level => the compression level to use when sending data
|
104
|
+
# * :config => set to +true+ to load the default OpenSSH config files
|
105
|
+
# (~/.ssh/config, /etc/ssh_config), or to +false+ to not load them, or to
|
106
|
+
# a file-name (or array of file-names) to load those specific configuration
|
107
|
+
# files. Defaults to +true+.
|
108
|
+
# * :encryption => the encryption cipher (or ciphers) to use
|
109
|
+
# * :forward_agent => set to true if you want the SSH agent connection to
|
110
|
+
# be forwarded
|
111
|
+
# * :global_known_hosts_file => the location of the global known hosts
|
112
|
+
# file. Set to an array if you want to specify multiple global known
|
113
|
+
# hosts files. Defaults to %w(/etc/ssh/known_hosts /etc/ssh/known_hosts2).
|
114
|
+
# * :hmac => the hmac algorithm (or algorithms) to use
|
115
|
+
# * :host_key => the host key algorithm (or algorithms) to use
|
116
|
+
# * :host_key_alias => the host name to use when looking up or adding a
|
117
|
+
# host to a known_hosts dictionary file
|
118
|
+
# * :host_name => the real host name or IP to log into. This is used
|
119
|
+
# instead of the +host+ parameter, and is primarily only useful when
|
120
|
+
# specified in an SSH configuration file. It lets you specify an
|
121
|
+
# "alias", similarly to adding an entry in /etc/hosts but without needing
|
122
|
+
# to modify /etc/hosts.
|
123
|
+
# * :kex => the key exchange algorithm (or algorithms) to use
|
124
|
+
# * :keys => an array of file names of private keys to use for publickey
|
125
|
+
# and hostbased authentication
|
126
|
+
# * :key_data => an array of strings, with each element of the array being
|
127
|
+
# a raw private key in PEM format.
|
128
|
+
# * :logger => the logger instance to use when logging
|
129
|
+
# * :paranoid => either true, false, or :very, specifying how strict
|
130
|
+
# host-key verification should be
|
131
|
+
# * :passphrase => the passphrase to use when loading a private key (default
|
132
|
+
# is +nil+, for no passphrase)
|
133
|
+
# * :password => the password to use to login
|
134
|
+
# * :port => the port to use when connecting to the remote host
|
135
|
+
# * :properties => a hash of key/value pairs to add to the new connection's
|
136
|
+
# properties (see Net::SSH::Connection::Session#properties)
|
137
|
+
# * :proxy => a proxy instance (see Proxy) to use when connecting
|
138
|
+
# * :rekey_blocks_limit => the max number of blocks to process before rekeying
|
139
|
+
# * :rekey_limit => the max number of bytes to process before rekeying
|
140
|
+
# * :rekey_packet_limit => the max number of packets to process before rekeying
|
141
|
+
# * :timeout => how long to wait for the initial connection to be made
|
142
|
+
# * :user => the user name to log in as; this overrides the +user+
|
143
|
+
# parameter, and is primarily only useful when provided via an SSH
|
144
|
+
# configuration file.
|
145
|
+
# * :user_known_hosts_file => the location of the user known hosts file.
|
146
|
+
# Set to an array to specify multiple user known hosts files.
|
147
|
+
# Defaults to %w(~/.ssh/known_hosts ~/.ssh/known_hosts2).
|
148
|
+
# * :verbose => how verbose to be (Logger verbosity constants, Logger::DEBUG
|
149
|
+
# is very verbose, Logger::FATAL is all but silent). Logger::FATAL is the
|
150
|
+
# default. The symbols :debug, :info, :warn, :error, and :fatal are also
|
151
|
+
# supported and are translated to the corresponding Logger constant.
|
152
|
+
def self.start(host, user, options={}, &block)
|
153
|
+
invalid_options = options.keys - VALID_OPTIONS
|
154
|
+
if invalid_options.any?
|
155
|
+
raise ArgumentError, "invalid option(s): #{invalid_options.join(', ')}"
|
156
|
+
end
|
157
|
+
|
158
|
+
options[:user] = user if user
|
159
|
+
options = configuration_for(host, options.fetch(:config, true)).merge(options)
|
160
|
+
host = options.fetch(:host_name, host)
|
161
|
+
|
162
|
+
if !options.key?(:logger)
|
163
|
+
options[:logger] = Logger.new(STDERR)
|
164
|
+
options[:logger].level = Logger::FATAL
|
165
|
+
end
|
166
|
+
|
167
|
+
if options[:verbose]
|
168
|
+
options[:logger].level = case options[:verbose]
|
169
|
+
when Fixnum then options[:verbose]
|
170
|
+
when :debug then Logger::DEBUG
|
171
|
+
when :info then Logger::INFO
|
172
|
+
when :warn then Logger::WARN
|
173
|
+
when :error then Logger::ERROR
|
174
|
+
when :fatal then Logger::FATAL
|
175
|
+
else raise ArgumentError, "can't convert #{options[:verbose].inspect} to any of the Logger level constants"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
transport = Transport::Session.new(host, options)
|
180
|
+
auth = Authentication::Session.new(transport, options)
|
181
|
+
|
182
|
+
user = options.fetch(:user, user)
|
183
|
+
if auth.authenticate("ssh-connection", user, options[:password])
|
184
|
+
connection = Connection::Session.new(transport, options)
|
185
|
+
if block_given?
|
186
|
+
yield connection
|
187
|
+
connection.close
|
188
|
+
else
|
189
|
+
return connection
|
190
|
+
end
|
191
|
+
else
|
192
|
+
raise AuthenticationFailed, user
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# Returns a hash of the configuration options for the given host, as read
|
197
|
+
# from the SSH configuration file(s). If +use_ssh_config+ is true (the
|
198
|
+
# default), this will load configuration from both ~/.ssh/config and
|
199
|
+
# /etc/ssh_config. If +use_ssh_config+ is nil or false, nothing will be
|
200
|
+
# loaded (and an empty hash returned). Otherwise, +use_ssh_config+ may
|
201
|
+
# be a file name (or array of file names) of SSH configuration file(s)
|
202
|
+
# to read.
|
203
|
+
#
|
204
|
+
# See Net::SSH::Config for the full description of all supported options.
|
205
|
+
def self.configuration_for(host, use_ssh_config=true)
|
206
|
+
files = case use_ssh_config
|
207
|
+
when true then Net::SSH::Config.default_files
|
208
|
+
when false, nil then return {}
|
209
|
+
else Array(use_ssh_config)
|
210
|
+
end
|
211
|
+
|
212
|
+
Net::SSH::Config.for(host, files)
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
require 'net/ssh/buffer'
|
2
|
+
require 'net/ssh/errors'
|
3
|
+
require 'net/ssh/loggable'
|
4
|
+
require 'net/ssh/transport/server_version'
|
5
|
+
|
6
|
+
require 'net/ssh/authentication/pageant' if File::ALT_SEPARATOR && !(RUBY_PLATFORM =~ /java/)
|
7
|
+
|
8
|
+
module Net; module SSH; module Authentication
|
9
|
+
|
10
|
+
# A trivial exception class for representing agent-specific errors.
|
11
|
+
class AgentError < Net::SSH::Exception; end
|
12
|
+
|
13
|
+
# An exception for indicating that the SSH agent is not available.
|
14
|
+
class AgentNotAvailable < AgentError; end
|
15
|
+
|
16
|
+
# This class implements a simple client for the ssh-agent protocol. It
|
17
|
+
# does not implement any specific protocol, but instead copies the
|
18
|
+
# behavior of the ssh-agent functions in the OpenSSH library (3.8).
|
19
|
+
#
|
20
|
+
# This means that although it behaves like a SSH1 client, it also has
|
21
|
+
# some SSH2 functionality (like signing data).
|
22
|
+
class Agent
|
23
|
+
include Loggable
|
24
|
+
|
25
|
+
# A simple module for extending keys, to allow comments to be specified
|
26
|
+
# for them.
|
27
|
+
module Comment
|
28
|
+
attr_accessor :comment
|
29
|
+
end
|
30
|
+
|
31
|
+
SSH2_AGENT_REQUEST_VERSION = 1
|
32
|
+
SSH2_AGENT_REQUEST_IDENTITIES = 11
|
33
|
+
SSH2_AGENT_IDENTITIES_ANSWER = 12
|
34
|
+
SSH2_AGENT_SIGN_REQUEST = 13
|
35
|
+
SSH2_AGENT_SIGN_RESPONSE = 14
|
36
|
+
SSH2_AGENT_FAILURE = 30
|
37
|
+
SSH2_AGENT_VERSION_RESPONSE = 103
|
38
|
+
|
39
|
+
SSH_COM_AGENT2_FAILURE = 102
|
40
|
+
|
41
|
+
SSH_AGENT_REQUEST_RSA_IDENTITIES = 1
|
42
|
+
SSH_AGENT_RSA_IDENTITIES_ANSWER1 = 2
|
43
|
+
SSH_AGENT_RSA_IDENTITIES_ANSWER2 = 5
|
44
|
+
SSH_AGENT_FAILURE = 5
|
45
|
+
|
46
|
+
# The underlying socket being used to communicate with the SSH agent.
|
47
|
+
attr_reader :socket
|
48
|
+
|
49
|
+
# Instantiates a new agent object, connects to a running SSH agent,
|
50
|
+
# negotiates the agent protocol version, and returns the agent object.
|
51
|
+
def self.connect(logger=nil)
|
52
|
+
agent = new(logger)
|
53
|
+
agent.connect!
|
54
|
+
agent.negotiate!
|
55
|
+
agent
|
56
|
+
end
|
57
|
+
|
58
|
+
# Creates a new Agent object, using the optional logger instance to
|
59
|
+
# report status.
|
60
|
+
def initialize(logger=nil)
|
61
|
+
self.logger = logger
|
62
|
+
end
|
63
|
+
|
64
|
+
# Connect to the agent process using the socket factory and socket name
|
65
|
+
# given by the attribute writers. If the agent on the other end of the
|
66
|
+
# socket reports that it is an SSH2-compatible agent, this will fail
|
67
|
+
# (it only supports the ssh-agent distributed by OpenSSH).
|
68
|
+
def connect!
|
69
|
+
begin
|
70
|
+
debug { "connecting to ssh-agent" }
|
71
|
+
@socket = agent_socket_factory.open(ENV['SSH_AUTH_SOCK'])
|
72
|
+
rescue
|
73
|
+
error { "could not connect to ssh-agent" }
|
74
|
+
raise AgentNotAvailable, $!.message
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Attempts to negotiate the SSH agent protocol version. Raises an error
|
79
|
+
# if the version could not be negotiated successfully.
|
80
|
+
def negotiate!
|
81
|
+
# determine what type of agent we're communicating with
|
82
|
+
type, body = send_and_wait(SSH2_AGENT_REQUEST_VERSION, :string, Transport::ServerVersion::PROTO_VERSION)
|
83
|
+
|
84
|
+
if type == SSH2_AGENT_VERSION_RESPONSE
|
85
|
+
raise NotImplementedError, "SSH2 agents are not yet supported"
|
86
|
+
elsif type != SSH_AGENT_RSA_IDENTITIES_ANSWER1 && type != SSH_AGENT_RSA_IDENTITIES_ANSWER2
|
87
|
+
raise AgentError, "unknown response from agent: #{type}, #{body.to_s.inspect}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return an array of all identities (public keys) known to the agent.
|
92
|
+
# Each key returned is augmented with a +comment+ property which is set
|
93
|
+
# to the comment returned by the agent for that key.
|
94
|
+
def identities
|
95
|
+
type, body = send_and_wait(SSH2_AGENT_REQUEST_IDENTITIES)
|
96
|
+
raise AgentError, "could not get identity count" if agent_failed(type)
|
97
|
+
raise AgentError, "bad authentication reply: #{type}" if type != SSH2_AGENT_IDENTITIES_ANSWER
|
98
|
+
|
99
|
+
identities = []
|
100
|
+
body.read_long.times do
|
101
|
+
key = Buffer.new(body.read_string).read_key
|
102
|
+
key.extend(Comment)
|
103
|
+
key.comment = body.read_string
|
104
|
+
identities.push key
|
105
|
+
end
|
106
|
+
|
107
|
+
return identities
|
108
|
+
end
|
109
|
+
|
110
|
+
# Closes this socket. This agent reference is no longer able to
|
111
|
+
# query the agent.
|
112
|
+
def close
|
113
|
+
@socket.close
|
114
|
+
end
|
115
|
+
|
116
|
+
# Using the agent and the given public key, sign the given data. The
|
117
|
+
# signature is returned in SSH2 format.
|
118
|
+
def sign(key, data)
|
119
|
+
type, reply = send_and_wait(SSH2_AGENT_SIGN_REQUEST, :string, Buffer.from(:key, key), :string, data, :long, 0)
|
120
|
+
|
121
|
+
if agent_failed(type)
|
122
|
+
raise AgentError, "agent could not sign data with requested identity"
|
123
|
+
elsif type != SSH2_AGENT_SIGN_RESPONSE
|
124
|
+
raise AgentError, "bad authentication response #{type}"
|
125
|
+
end
|
126
|
+
|
127
|
+
return reply.read_string
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
# Returns the agent socket factory to use.
|
133
|
+
def agent_socket_factory
|
134
|
+
if File::ALT_SEPARATOR
|
135
|
+
Pageant::Socket
|
136
|
+
else
|
137
|
+
UNIXSocket
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Send a new packet of the given type, with the associated data.
|
142
|
+
def send_packet(type, *args)
|
143
|
+
buffer = Buffer.from(*args)
|
144
|
+
data = [buffer.length + 1, type.to_i, buffer.to_s].pack("NCA*")
|
145
|
+
debug { "sending agent request #{type} len #{buffer.length}" }
|
146
|
+
@socket.send data, 0
|
147
|
+
end
|
148
|
+
|
149
|
+
# Read the next packet from the agent. This will return a two-part
|
150
|
+
# tuple consisting of the packet type, and the packet's body (which
|
151
|
+
# is returned as a Net::SSH::Buffer).
|
152
|
+
def read_packet
|
153
|
+
buffer = Net::SSH::Buffer.new(@socket.read(4))
|
154
|
+
buffer.append(@socket.read(buffer.read_long))
|
155
|
+
type = buffer.read_byte
|
156
|
+
debug { "received agent packet #{type} len #{buffer.length-4}" }
|
157
|
+
return type, buffer
|
158
|
+
end
|
159
|
+
|
160
|
+
# Send the given packet and return the subsequent reply from the agent.
|
161
|
+
# (See #send_packet and #read_packet).
|
162
|
+
def send_and_wait(type, *args)
|
163
|
+
send_packet(type, *args)
|
164
|
+
read_packet
|
165
|
+
end
|
166
|
+
|
167
|
+
# Returns +true+ if the parameter indicates a "failure" response from
|
168
|
+
# the agent, and +false+ otherwise.
|
169
|
+
def agent_failed(type)
|
170
|
+
type == SSH_AGENT_FAILURE ||
|
171
|
+
type == SSH2_AGENT_FAILURE ||
|
172
|
+
type == SSH_COM_AGENT2_FAILURE
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
end; end; end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Net; module SSH; module Authentication
|
2
|
+
|
3
|
+
# Describes the constants used by the Net::SSH::Authentication components
|
4
|
+
# of the Net::SSH library. Individual authentication method implemenations
|
5
|
+
# may define yet more constants that are specific to their implementation.
|
6
|
+
module Constants
|
7
|
+
USERAUTH_REQUEST = 50
|
8
|
+
USERAUTH_FAILURE = 51
|
9
|
+
USERAUTH_SUCCESS = 52
|
10
|
+
USERAUTH_BANNER = 53
|
11
|
+
|
12
|
+
USERAUTH_PASSWD_CHANGEREQ = 60
|
13
|
+
USERAUTH_PK_OK = 60
|
14
|
+
|
15
|
+
USERAUTH_METHOD_RANGE = 60..79
|
16
|
+
end
|
17
|
+
|
18
|
+
end; end; end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'net/ssh/errors'
|
2
|
+
require 'net/ssh/key_factory'
|
3
|
+
require 'net/ssh/loggable'
|
4
|
+
require 'net/ssh/authentication/agent'
|
5
|
+
|
6
|
+
module Net
|
7
|
+
module SSH
|
8
|
+
module Authentication
|
9
|
+
|
10
|
+
# A trivial exception class used to report errors in the key manager.
|
11
|
+
class KeyManagerError < Net::SSH::Exception; end
|
12
|
+
|
13
|
+
# This class encapsulates all operations done by clients on a user's
|
14
|
+
# private keys. In practice, the client should never need a reference
|
15
|
+
# to a private key; instead, they grab a list of "identities" (public
|
16
|
+
# keys) that are available from the KeyManager, and then use
|
17
|
+
# the KeyManager to do various private key operations using those
|
18
|
+
# identities.
|
19
|
+
#
|
20
|
+
# The KeyManager also uses the Agent class to encapsulate the
|
21
|
+
# ssh-agent. Thus, from a client's perspective it is completely
|
22
|
+
# hidden whether an identity comes from the ssh-agent or from a file
|
23
|
+
# on disk.
|
24
|
+
class KeyManager
|
25
|
+
include Loggable
|
26
|
+
|
27
|
+
# The list of user key files that will be examined
|
28
|
+
attr_reader :key_files
|
29
|
+
|
30
|
+
# The list of user key data that will be examined
|
31
|
+
attr_reader :key_data
|
32
|
+
|
33
|
+
# The map of loaded identities
|
34
|
+
attr_reader :known_identities
|
35
|
+
|
36
|
+
# The map of options that were passed to the key-manager
|
37
|
+
attr_reader :options
|
38
|
+
|
39
|
+
# Create a new KeyManager. By default, the manager will
|
40
|
+
# use the ssh-agent (if it is running).
|
41
|
+
def initialize(logger, options={})
|
42
|
+
self.logger = logger
|
43
|
+
@key_files = []
|
44
|
+
@key_data = []
|
45
|
+
@use_agent = true
|
46
|
+
@known_identities = {}
|
47
|
+
@agent = nil
|
48
|
+
@options = options
|
49
|
+
end
|
50
|
+
|
51
|
+
# Clear all knowledge of any loaded user keys. This also clears the list
|
52
|
+
# of default identity files that are to be loaded, thus making it
|
53
|
+
# appropriate to use if a client wishes to NOT use the default identity
|
54
|
+
# files.
|
55
|
+
def clear!
|
56
|
+
key_files.clear
|
57
|
+
key_data.clear
|
58
|
+
known_identities.clear
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
# Add the given key_file to the list of key files that will be used.
|
63
|
+
def add(key_file)
|
64
|
+
key_files.push(File.expand_path(key_file)).uniq!
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
# Add the given key_file to the list of keys that will be used.
|
69
|
+
def add_key_data(key_data_)
|
70
|
+
key_data.push(key_data_).uniq!
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
# This is used as a hint to the KeyManager indicating that the agent
|
75
|
+
# connection is no longer needed. Any other open resources may be closed
|
76
|
+
# at this time.
|
77
|
+
#
|
78
|
+
# Calling this does NOT indicate that the KeyManager will no longer
|
79
|
+
# be used. Identities may still be requested and operations done on
|
80
|
+
# loaded identities, in which case, the agent will be automatically
|
81
|
+
# reconnected. This method simply allows the client connection to be
|
82
|
+
# closed when it will not be used in the immediate future.
|
83
|
+
def finish
|
84
|
+
@agent.close if @agent
|
85
|
+
@agent = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
# Iterates over all available identities (public keys) known to this
|
89
|
+
# manager. As it finds one, it will then yield it to the caller.
|
90
|
+
# The origin of the identities may be from files on disk or from an
|
91
|
+
# ssh-agent. Note that identities from an ssh-agent are always listed
|
92
|
+
# first in the array, with other identities coming after.
|
93
|
+
def each_identity
|
94
|
+
if agent
|
95
|
+
agent.identities.each do |key|
|
96
|
+
known_identities[key] = { :from => :agent }
|
97
|
+
yield key
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
key_files.each do |file|
|
102
|
+
public_key_file = file + ".pub"
|
103
|
+
if File.readable?(public_key_file)
|
104
|
+
begin
|
105
|
+
key = KeyFactory.load_public_key(public_key_file)
|
106
|
+
known_identities[key] = { :from => :file, :file => file }
|
107
|
+
yield key
|
108
|
+
rescue Exception => e
|
109
|
+
error { "could not load public key file `#{public_key_file}': #{e.class} (#{e.message})" }
|
110
|
+
end
|
111
|
+
elsif File.readable?(file)
|
112
|
+
begin
|
113
|
+
private_key = KeyFactory.load_private_key(file, options[:passphrase])
|
114
|
+
key = private_key.send(:public_key)
|
115
|
+
known_identities[key] = { :from => :file, :file => file, :key => private_key }
|
116
|
+
yield key
|
117
|
+
rescue Exception => e
|
118
|
+
error { "could not load private key file `#{file}': #{e.class} (#{e.message})" }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
key_data.each do |data|
|
124
|
+
private_key = KeyFactory.load_data_private_key(data)
|
125
|
+
key = private_key.send(:public_key)
|
126
|
+
known_identities[key] = { :from => :key_data, :data => data, :key => private_key }
|
127
|
+
yield key
|
128
|
+
end
|
129
|
+
|
130
|
+
self
|
131
|
+
end
|
132
|
+
|
133
|
+
# Sign the given data, using the corresponding private key of the given
|
134
|
+
# identity. If the identity was originally obtained from an ssh-agent,
|
135
|
+
# then the ssh-agent will be used to sign the data, otherwise the
|
136
|
+
# private key for the identity will be loaded from disk (if it hasn't
|
137
|
+
# been loaded already) and will then be used to sign the data.
|
138
|
+
#
|
139
|
+
# Regardless of the identity's origin or who does the signing, this
|
140
|
+
# will always return the signature in an SSH2-specified "signature
|
141
|
+
# blob" format.
|
142
|
+
def sign(identity, data)
|
143
|
+
info = known_identities[identity] or raise KeyManagerError, "the given identity is unknown to the key manager"
|
144
|
+
|
145
|
+
if info[:key].nil? && info[:from] == :file
|
146
|
+
begin
|
147
|
+
info[:key] = KeyFactory.load_private_key(info[:file], options[:passphrase])
|
148
|
+
rescue Exception => e
|
149
|
+
raise KeyManagerError, "the given identity is known, but the private key could not be loaded: #{e.class} (#{e.message})"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
if info[:key]
|
154
|
+
return Net::SSH::Buffer.from(:string, identity.ssh_type,
|
155
|
+
:string, info[:key].ssh_do_sign(data.to_s)).to_s
|
156
|
+
end
|
157
|
+
|
158
|
+
if info[:from] == :agent
|
159
|
+
raise KeyManagerError, "the agent is no longer available" unless agent
|
160
|
+
return agent.sign(identity, data.to_s)
|
161
|
+
end
|
162
|
+
|
163
|
+
raise KeyManagerError, "[BUG] can't determine identity origin (#{info.inspect})"
|
164
|
+
end
|
165
|
+
|
166
|
+
# Identifies whether the ssh-agent will be used or not.
|
167
|
+
def use_agent?
|
168
|
+
@use_agent
|
169
|
+
end
|
170
|
+
|
171
|
+
# Toggles whether the ssh-agent will be used or not. If true, an
|
172
|
+
# attempt will be made to use the ssh-agent. If false, any existing
|
173
|
+
# connection to an agent is closed and the agent will not be used.
|
174
|
+
def use_agent=(use_agent)
|
175
|
+
finish if !use_agent
|
176
|
+
@use_agent = use_agent
|
177
|
+
end
|
178
|
+
|
179
|
+
# Returns an Agent instance to use for communicating with an SSH
|
180
|
+
# agent process. Returns nil if use of an SSH agent has been disabled,
|
181
|
+
# or if the agent is otherwise not available.
|
182
|
+
def agent
|
183
|
+
return unless use_agent?
|
184
|
+
@agent ||= Agent.connect(logger)
|
185
|
+
rescue AgentNotAvailable
|
186
|
+
@use_agent = false
|
187
|
+
nil
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|