net-ssh 4.1.0 → 6.1.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.
Files changed (111) hide show
  1. checksums.yaml +5 -5
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/.gitignore +5 -0
  5. data/.rubocop.yml +8 -2
  6. data/.rubocop_todo.yml +405 -552
  7. data/.travis.yml +23 -22
  8. data/CHANGES.txt +112 -1
  9. data/Gemfile +1 -7
  10. data/{Gemfile.norbnacl → Gemfile.noed25519} +1 -1
  11. data/Manifest +4 -5
  12. data/README.md +287 -0
  13. data/Rakefile +40 -29
  14. data/appveyor.yml +12 -6
  15. data/lib/net/ssh.rb +68 -32
  16. data/lib/net/ssh/authentication/agent.rb +234 -222
  17. data/lib/net/ssh/authentication/certificate.rb +175 -164
  18. data/lib/net/ssh/authentication/constants.rb +17 -14
  19. data/lib/net/ssh/authentication/ed25519.rb +162 -141
  20. data/lib/net/ssh/authentication/ed25519_loader.rb +32 -29
  21. data/lib/net/ssh/authentication/key_manager.rb +40 -9
  22. data/lib/net/ssh/authentication/methods/abstract.rb +53 -47
  23. data/lib/net/ssh/authentication/methods/hostbased.rb +32 -33
  24. data/lib/net/ssh/authentication/methods/keyboard_interactive.rb +1 -1
  25. data/lib/net/ssh/authentication/methods/none.rb +10 -10
  26. data/lib/net/ssh/authentication/methods/password.rb +13 -13
  27. data/lib/net/ssh/authentication/methods/publickey.rb +56 -55
  28. data/lib/net/ssh/authentication/pageant.rb +468 -465
  29. data/lib/net/ssh/authentication/pub_key_fingerprint.rb +43 -0
  30. data/lib/net/ssh/authentication/session.rb +130 -122
  31. data/lib/net/ssh/buffer.rb +345 -312
  32. data/lib/net/ssh/buffered_io.rb +163 -163
  33. data/lib/net/ssh/config.rb +316 -238
  34. data/lib/net/ssh/connection/channel.rb +670 -650
  35. data/lib/net/ssh/connection/constants.rb +30 -26
  36. data/lib/net/ssh/connection/event_loop.rb +108 -105
  37. data/lib/net/ssh/connection/keepalive.rb +54 -50
  38. data/lib/net/ssh/connection/session.rb +682 -671
  39. data/lib/net/ssh/connection/term.rb +180 -176
  40. data/lib/net/ssh/errors.rb +101 -99
  41. data/lib/net/ssh/key_factory.rb +195 -108
  42. data/lib/net/ssh/known_hosts.rb +161 -152
  43. data/lib/net/ssh/loggable.rb +57 -55
  44. data/lib/net/ssh/packet.rb +82 -78
  45. data/lib/net/ssh/prompt.rb +55 -53
  46. data/lib/net/ssh/proxy/command.rb +104 -89
  47. data/lib/net/ssh/proxy/errors.rb +12 -8
  48. data/lib/net/ssh/proxy/http.rb +93 -91
  49. data/lib/net/ssh/proxy/https.rb +42 -39
  50. data/lib/net/ssh/proxy/jump.rb +50 -47
  51. data/lib/net/ssh/proxy/socks4.rb +0 -2
  52. data/lib/net/ssh/proxy/socks5.rb +11 -12
  53. data/lib/net/ssh/service/forward.rb +370 -317
  54. data/lib/net/ssh/test.rb +83 -77
  55. data/lib/net/ssh/test/channel.rb +146 -142
  56. data/lib/net/ssh/test/extensions.rb +150 -146
  57. data/lib/net/ssh/test/kex.rb +35 -31
  58. data/lib/net/ssh/test/local_packet.rb +48 -44
  59. data/lib/net/ssh/test/packet.rb +87 -84
  60. data/lib/net/ssh/test/remote_packet.rb +35 -31
  61. data/lib/net/ssh/test/script.rb +173 -171
  62. data/lib/net/ssh/test/socket.rb +59 -55
  63. data/lib/net/ssh/transport/algorithms.rb +430 -364
  64. data/lib/net/ssh/transport/cipher_factory.rb +95 -91
  65. data/lib/net/ssh/transport/constants.rb +33 -25
  66. data/lib/net/ssh/transport/ctr.rb +33 -11
  67. data/lib/net/ssh/transport/hmac.rb +15 -13
  68. data/lib/net/ssh/transport/hmac/abstract.rb +82 -63
  69. data/lib/net/ssh/transport/hmac/sha2_256.rb +7 -11
  70. data/lib/net/ssh/transport/hmac/sha2_256_96.rb +4 -8
  71. data/lib/net/ssh/transport/hmac/sha2_256_etm.rb +12 -0
  72. data/lib/net/ssh/transport/hmac/sha2_512.rb +6 -9
  73. data/lib/net/ssh/transport/hmac/sha2_512_96.rb +4 -8
  74. data/lib/net/ssh/transport/hmac/sha2_512_etm.rb +12 -0
  75. data/lib/net/ssh/transport/identity_cipher.rb +55 -51
  76. data/lib/net/ssh/transport/kex.rb +14 -13
  77. data/lib/net/ssh/transport/kex/abstract.rb +123 -0
  78. data/lib/net/ssh/transport/kex/abstract5656.rb +72 -0
  79. data/lib/net/ssh/transport/kex/curve25519_sha256.rb +38 -0
  80. data/lib/net/ssh/transport/kex/curve25519_sha256_loader.rb +30 -0
  81. data/lib/net/ssh/transport/kex/diffie_hellman_group14_sha1.rb +33 -40
  82. data/lib/net/ssh/transport/kex/diffie_hellman_group1_sha1.rb +112 -217
  83. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha1.rb +53 -62
  84. data/lib/net/ssh/transport/kex/diffie_hellman_group_exchange_sha256.rb +5 -9
  85. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp256.rb +36 -90
  86. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp384.rb +18 -10
  87. data/lib/net/ssh/transport/kex/ecdh_sha2_nistp521.rb +18 -10
  88. data/lib/net/ssh/transport/key_expander.rb +29 -25
  89. data/lib/net/ssh/transport/openssl.rb +116 -116
  90. data/lib/net/ssh/transport/packet_stream.rb +223 -190
  91. data/lib/net/ssh/transport/server_version.rb +64 -66
  92. data/lib/net/ssh/transport/session.rb +306 -257
  93. data/lib/net/ssh/transport/state.rb +198 -196
  94. data/lib/net/ssh/verifiers/accept_new.rb +35 -0
  95. data/lib/net/ssh/verifiers/accept_new_or_local_tunnel.rb +34 -0
  96. data/lib/net/ssh/verifiers/always.rb +56 -0
  97. data/lib/net/ssh/verifiers/never.rb +21 -0
  98. data/lib/net/ssh/version.rb +55 -53
  99. data/net-ssh-public_cert.pem +18 -19
  100. data/net-ssh.gemspec +12 -11
  101. data/support/ssh_tunnel_bug.rb +2 -2
  102. metadata +86 -75
  103. metadata.gz.sig +0 -0
  104. data/Gemfile.norbnacl.lock +0 -41
  105. data/README.rdoc +0 -169
  106. data/lib/net/ssh/ruby_compat.rb +0 -24
  107. data/lib/net/ssh/verifiers/lenient.rb +0 -30
  108. data/lib/net/ssh/verifiers/null.rb +0 -12
  109. data/lib/net/ssh/verifiers/secure.rb +0 -52
  110. data/lib/net/ssh/verifiers/strict.rb +0 -24
  111. data/support/arcfour_check.rb +0 -20
@@ -3,130 +3,217 @@ require 'net/ssh/prompt'
3
3
 
4
4
  require 'net/ssh/authentication/ed25519_loader'
5
5
 
6
- module Net; module SSH
7
-
8
- # A factory class for returning new Key classes. It is used for obtaining
9
- # OpenSSL key instances via their SSH names, and for loading both public and
10
- # private keys. It used used primarily by Net::SSH itself, internally, and
11
- # will rarely (if ever) be directly used by consumers of the library.
12
- #
13
- # klass = Net::SSH::KeyFactory.get("rsa")
14
- # assert klass.is_a?(OpenSSL::PKey::RSA)
15
- #
16
- # key = Net::SSH::KeyFactory.load_public_key("~/.ssh/id_dsa.pub")
17
- class KeyFactory
18
- # Specifies the mapping of SSH names to OpenSSL key classes.
19
- MAP = {
20
- "dh" => OpenSSL::PKey::DH,
21
- "rsa" => OpenSSL::PKey::RSA,
22
- "dsa" => OpenSSL::PKey::DSA,
23
- }
24
- if defined?(OpenSSL::PKey::EC)
25
- MAP["ecdsa"] = OpenSSL::PKey::EC
6
+ module Net
7
+ module SSH
8
+
9
+ # A factory class for returning new Key classes. It is used for obtaining
10
+ # OpenSSL key instances via their SSH names, and for loading both public and
11
+ # private keys. It used used primarily by Net::SSH itself, internally, and
12
+ # will rarely (if ever) be directly used by consumers of the library.
13
+ #
14
+ # klass = Net::SSH::KeyFactory.get("rsa")
15
+ # assert klass.is_a?(OpenSSL::PKey::RSA)
16
+ #
17
+ # key = Net::SSH::KeyFactory.load_public_key("~/.ssh/id_dsa.pub")
18
+ class KeyFactory
19
+ # Specifies the mapping of SSH names to OpenSSL key classes.
20
+ MAP = {
21
+ 'dh' => OpenSSL::PKey::DH,
22
+ 'rsa' => OpenSSL::PKey::RSA,
23
+ 'dsa' => OpenSSL::PKey::DSA,
24
+ 'ecdsa' => OpenSSL::PKey::EC
25
+ }
26
26
  MAP["ed25519"] = Net::SSH::Authentication::ED25519::PrivKey if defined? Net::SSH::Authentication::ED25519
27
- end
28
-
29
- class <<self
30
- # Fetch an OpenSSL key instance by its SSH name. It will be a new,
31
- # empty key of the given type.
32
- def get(name)
33
- MAP.fetch(name).new
34
- end
35
-
36
- # Loads a private key from a file. It will correctly determine
37
- # whether the file describes an RSA or DSA key, and will load it
38
- # appropriately. The new key is returned. If the key itself is
39
- # encrypted (requiring a passphrase to use), the user will be
40
- # prompted to enter their password unless passphrase works.
41
- def load_private_key(filename, passphrase=nil, ask_passphrase=true, prompt=Prompt.default)
42
- data = File.read(File.expand_path(filename))
43
- load_data_private_key(data, passphrase, ask_passphrase, filename, prompt)
44
- end
45
27
 
46
- # Loads a private key. It will correctly determine
47
- # whether the file describes an RSA or DSA key, and will load it
48
- # appropriately. The new key is returned. If the key itself is
49
- # encrypted (requiring a passphrase to use), the user will be
50
- # prompted to enter their password unless passphrase works.
51
- def load_data_private_key(data, passphrase=nil, ask_passphrase=true, filename="", prompt=Prompt.default)
52
- key_read, error_classes = classify_key(data, filename)
28
+ class <<self
29
+ # Fetch an OpenSSL key instance by its SSH name. It will be a new,
30
+ # empty key of the given type.
31
+ def get(name)
32
+ MAP.fetch(name).new
33
+ end
53
34
 
54
- encrypted_key = data.match(/ENCRYPTED/)
55
- tries = 0
35
+ # Loads a private key from a file. It will correctly determine
36
+ # whether the file describes an RSA or DSA key, and will load it
37
+ # appropriately. The new key is returned. If the key itself is
38
+ # encrypted (requiring a passphrase to use), the user will be
39
+ # prompted to enter their password unless passphrase works.
40
+ def load_private_key(filename, passphrase=nil, ask_passphrase=true, prompt=Prompt.default)
41
+ data = File.read(File.expand_path(filename))
42
+ load_data_private_key(data, passphrase, ask_passphrase, filename, prompt)
43
+ end
56
44
 
57
- prompter = nil
58
- result =
59
- begin
60
- key_read[data, passphrase || 'invalid']
61
- rescue *error_classes
62
- if encrypted_key && ask_passphrase
63
- tries += 1
64
- if tries <= 3
65
- prompter ||= prompt.start(type: 'private_key', filename: filename, sha: Digest::SHA256.digest(data))
66
- passphrase = prompter.ask("Enter passphrase for #{filename}:", false)
67
- retry
45
+ # Loads a private key. It will correctly determine
46
+ # whether the file describes an RSA or DSA key, and will load it
47
+ # appropriately. The new key is returned. If the key itself is
48
+ # encrypted (requiring a passphrase to use), the user will be
49
+ # prompted to enter their password unless passphrase works.
50
+ def load_data_private_key(data, passphrase=nil, ask_passphrase=true, filename="", prompt=Prompt.default)
51
+ key_type = classify_key(data, filename)
52
+
53
+ encrypted_key = nil
54
+ tries = 0
55
+
56
+ prompter = nil
57
+ result =
58
+ begin
59
+ key_type.read(data, passphrase || 'invalid')
60
+ rescue *key_type.error_classes => e
61
+ encrypted_key = !!key_type.encrypted_key?(data, e) if encrypted_key.nil?
62
+ if encrypted_key && ask_passphrase
63
+ tries += 1
64
+ if tries <= 3
65
+ prompter ||= prompt.start(type: 'private_key', filename: filename, sha: Digest::SHA256.digest(data))
66
+ passphrase = prompter.ask("Enter passphrase for #{filename}:", false)
67
+ retry
68
+ else
69
+ raise
70
+ end
68
71
  else
69
72
  raise
70
73
  end
71
- else
72
- raise
73
74
  end
74
- end
75
- prompter.success if prompter
76
- result
77
- end
75
+ prompter.success if prompter
76
+ result
77
+ end
78
78
 
79
- # Loads a public key from a file. It will correctly determine whether
80
- # the file describes an RSA or DSA key, and will load it
81
- # appropriately. The new public key is returned.
82
- def load_public_key(filename)
83
- data = File.read(File.expand_path(filename))
84
- load_data_public_key(data, filename)
85
- end
79
+ # Loads a public key from a file. It will correctly determine whether
80
+ # the file describes an RSA or DSA key, and will load it
81
+ # appropriately. The new public key is returned.
82
+ def load_public_key(filename)
83
+ data = File.read(File.expand_path(filename))
84
+ load_data_public_key(data, filename)
85
+ end
86
86
 
87
- # Loads a public key. It will correctly determine whether
88
- # the file describes an RSA or DSA key, and will load it
89
- # appropriately. The new public key is returned.
90
- def load_data_public_key(data, filename="")
91
- fields = data.split(/ /)
87
+ # Loads a public key. It will correctly determine whether
88
+ # the file describes an RSA or DSA key, and will load it
89
+ # appropriately. The new public key is returned.
90
+ def load_data_public_key(data, filename="")
91
+ fields = data.split(/ /)
92
92
 
93
- blob = nil
94
- begin
93
+ blob = nil
94
+ begin
95
+ blob = fields.shift
96
+ end while !blob.nil? && !/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match(blob)
95
97
  blob = fields.shift
96
- end while !blob.nil? && !/^(ssh-(rsa|dss|ed25519)|ecdsa-sha2-nistp\d+)(-cert-v01@openssh\.com)?$/.match(blob)
97
- blob = fields.shift
98
98
 
99
- raise Net::SSH::Exception, "public key at #{filename} is not valid" if blob.nil?
99
+ raise Net::SSH::Exception, "public key at #{filename} is not valid" if blob.nil?
100
100
 
101
- blob = blob.unpack("m*").first
102
- reader = Net::SSH::Buffer.new(blob)
103
- reader.read_key or raise OpenSSL::PKey::PKeyError, "not a public key #{filename.inspect}"
104
- end
101
+ blob = blob.unpack("m*").first
102
+ reader = Net::SSH::Buffer.new(blob)
103
+ reader.read_key or raise OpenSSL::PKey::PKeyError, "not a public key #{filename.inspect}"
104
+ end
105
+
106
+ private
107
+
108
+ # rubocop:disable Style/Documentation, Lint/DuplicateMethods
109
+ class KeyType
110
+ def self.read(key_data, passphrase)
111
+ raise Exception, "TODO subclasses should implement read"
112
+ end
113
+
114
+ def self.error_classes
115
+ raise Exception, "TODO subclasses should implement read"
116
+ end
117
+
118
+ def self.encrypted_key?(data, error)
119
+ raise Exception, "TODO subclasses should implement is_encrypted_key"
120
+ end
121
+ end
122
+
123
+ class OpenSSHPrivateKeyType < KeyType
124
+ def self.read(key_data, passphrase)
125
+ Net::SSH::Authentication::ED25519::OpenSSHPrivateKeyLoader.read(key_data, passphrase)
126
+ end
127
+
128
+ def self.error_classes
129
+ [Net::SSH::Authentication::ED25519::OpenSSHPrivateKeyLoader::DecryptError]
130
+ end
131
+
132
+ def self.encrypted_key?(key_data, decode_error)
133
+ decode_error.is_a?(Net::SSH::Authentication::ED25519::OpenSSHPrivateKeyLoader::DecryptError) && decode_error.encrypted_key?
134
+ end
135
+ end
105
136
 
106
- private
107
-
108
- # Determine whether the file describes an RSA or DSA key, and return how load it
109
- # appropriately.
110
- def classify_key(data, filename)
111
- if data.match(/-----BEGIN OPENSSH PRIVATE KEY-----/)
112
- Net::SSH::Authentication::ED25519Loader.raiseUnlessLoaded("OpenSSH keys only supported if ED25519 is available")
113
- return ->(key_data, passphrase) { Net::SSH::Authentication::ED25519::PrivKey.read(key_data, passphrase) }, [ArgumentError]
114
- elsif OpenSSL::PKey.respond_to?(:read)
115
- return ->(key_data, passphrase) { OpenSSL::PKey.read(key_data, passphrase) }, [ArgumentError, OpenSSL::PKey::PKeyError]
116
- elsif data.match(/-----BEGIN DSA PRIVATE KEY-----/)
117
- return ->(key_data, passphrase) { OpenSSL::PKey::DSA.new(key_data, passphrase) }, [OpenSSL::PKey::DSAError]
118
- elsif data.match(/-----BEGIN RSA PRIVATE KEY-----/)
119
- return ->(key_data, passphrase) { OpenSSL::PKey::RSA.new(key_data, passphrase) }, [OpenSSL::PKey::RSAError]
120
- elsif data.match(/-----BEGIN EC PRIVATE KEY-----/) && defined?(OpenSSL::PKey::EC)
121
- return ->(key_data, passphrase) { OpenSSL::PKey::EC.new(key_data, passphrase) }, [OpenSSL::PKey::ECError]
122
- elsif data.match(/-----BEGIN (.+) PRIVATE KEY-----/)
123
- raise OpenSSL::PKey::PKeyError, "not a supported key type '#{$1}'"
124
- else
125
- raise OpenSSL::PKey::PKeyError, "not a private key (#{filename})"
137
+ class OpenSSLKeyTypeBase < KeyType
138
+ def self.open_ssl_class
139
+ raise Exception, "TODO: subclasses should implement"
140
+ end
141
+
142
+ def self.read(key_data, passphrase)
143
+ open_ssl_class.new(key_data, passphrase)
144
+ end
145
+
146
+ def self.encrypted_key?(key_data, error)
147
+ key_data.match(/ENCRYPTED/)
148
+ end
149
+ end
150
+
151
+ class OpenSSLPKeyType < OpenSSLKeyTypeBase
152
+ def self.read(key_data, passphrase)
153
+ open_ssl_class.read(key_data, passphrase)
154
+ end
155
+
156
+ def self.open_ssl_class
157
+ OpenSSL::PKey
158
+ end
159
+
160
+ def self.error_classes
161
+ [ArgumentError, OpenSSL::PKey::PKeyError]
162
+ end
163
+ end
164
+
165
+ class OpenSSLDSAKeyType < OpenSSLKeyTypeBase
166
+ def self.open_ssl_class
167
+ OpenSSL::PKey::DSA
168
+ end
169
+
170
+ def self.error_classes
171
+ [OpenSSL::PKey::DSAError]
172
+ end
173
+ end
174
+
175
+ class OpenSSLRSAKeyType < OpenSSLKeyTypeBase
176
+ def self.open_ssl_class
177
+ OpenSSL::PKey::RSA
178
+ end
179
+
180
+ def self.error_classes
181
+ [OpenSSL::PKey::RSAError]
182
+ end
183
+ end
184
+
185
+ class OpenSSLECKeyType < OpenSSLKeyTypeBase
186
+ def self.open_ssl_class
187
+ OpenSSL::PKey::EC
188
+ end
189
+
190
+ def self.error_classes
191
+ [OpenSSL::PKey::ECError]
192
+ end
193
+ end
194
+ # rubocop:enable Style/Documentation, Lint/DuplicateMethods
195
+
196
+ # Determine whether the file describes an RSA or DSA key, and return how load it
197
+ # appropriately.
198
+ def classify_key(data, filename)
199
+ if data.match(/-----BEGIN OPENSSH PRIVATE KEY-----/)
200
+ Net::SSH::Authentication::ED25519Loader.raiseUnlessLoaded("OpenSSH keys only supported if ED25519 is available")
201
+ return OpenSSHPrivateKeyType
202
+ elsif OpenSSL::PKey.respond_to?(:read)
203
+ return OpenSSLPKeyType
204
+ elsif data.match(/-----BEGIN DSA PRIVATE KEY-----/)
205
+ return OpenSSLDSAKeyType
206
+ elsif data.match(/-----BEGIN RSA PRIVATE KEY-----/)
207
+ return OpenSSLRSAKeyType
208
+ elsif data.match(/-----BEGIN EC PRIVATE KEY-----/)
209
+ return OpenSSLECKeyType
210
+ elsif data.match(/-----BEGIN (.+) PRIVATE KEY-----/)
211
+ raise OpenSSL::PKey::PKeyError, "not a supported key type '#{$1}'"
212
+ else
213
+ raise OpenSSL::PKey::PKeyError, "not a private key (#{filename})"
214
+ end
126
215
  end
127
216
  end
128
217
  end
129
-
130
218
  end
131
-
132
- end; end
219
+ end
@@ -2,186 +2,195 @@ require 'strscan'
2
2
  require 'openssl'
3
3
  require 'base64'
4
4
  require 'net/ssh/buffer'
5
+ require 'net/ssh/authentication/ed25519_loader'
6
+
7
+ module Net
8
+ module SSH
9
+
10
+ # Represents the result of a search in known hosts
11
+ # see search_for
12
+ class HostKeys
13
+ include Enumerable
14
+ attr_reader :host
15
+
16
+ def initialize(host_keys, host, known_hosts, options = {})
17
+ @host_keys = host_keys
18
+ @host = host
19
+ @known_hosts = known_hosts
20
+ @options = options
21
+ end
5
22
 
6
- module Net; module SSH
7
-
8
- # Represents the result of a search in known hosts
9
- # see search_for
10
- class HostKeys
11
- include Enumerable
12
- attr_reader :host
13
-
14
- def initialize(host_keys, host, known_hosts, options = {})
15
- @host_keys = host_keys
16
- @host = host
17
- @known_hosts = known_hosts
18
- @options = options
19
- end
23
+ def add_host_key(key)
24
+ @known_hosts.add(@host, key, @options)
25
+ @host_keys.push(key)
26
+ end
20
27
 
21
- def add_host_key(key)
22
- @known_hosts.add(@host, key, @options)
23
- @host_keys.push(key)
24
- end
28
+ def each(&block)
29
+ @host_keys.each(&block)
30
+ end
25
31
 
26
- def each(&block)
27
- @host_keys.each(&block)
32
+ def empty?
33
+ @host_keys.empty?
34
+ end
28
35
  end
29
36
 
30
- def empty?
31
- @host_keys.empty?
32
- end
33
- end
34
-
35
- # Searches an OpenSSH-style known-host file for a given host, and returns all
36
- # matching keys. This is used to implement host-key verification, as well as
37
- # to determine what key a user prefers to use for a given host.
38
- #
39
- # This is used internally by Net::SSH, and will never need to be used directly
40
- # by consumers of the library.
41
- class KnownHosts
42
-
43
- if defined?(OpenSSL::PKey::EC)
44
- SUPPORTED_TYPE = %w(ssh-rsa ssh-dss
37
+ # Searches an OpenSSH-style known-host file for a given host, and returns all
38
+ # matching keys. This is used to implement host-key verification, as well as
39
+ # to determine what key a user prefers to use for a given host.
40
+ #
41
+ # This is used internally by Net::SSH, and will never need to be used directly
42
+ # by consumers of the library.
43
+ class KnownHosts
44
+ SUPPORTED_TYPE = %w[ssh-rsa ssh-dss
45
45
  ecdsa-sha2-nistp256
46
46
  ecdsa-sha2-nistp384
47
- ecdsa-sha2-nistp521)
48
- else
49
- SUPPORTED_TYPE = %w(ssh-rsa ssh-dss)
50
- end
51
-
47
+ ecdsa-sha2-nistp521]
52
48
 
53
- class <<self
49
+ SUPPORTED_TYPE.push('ssh-ed25519') if Net::SSH::Authentication::ED25519Loader::LOADED
54
50
 
55
- # Searches all known host files (see KnownHosts.hostfiles) for all keys
56
- # of the given host. Returns an enumerable of keys found.
57
- def search_for(host, options={})
58
- HostKeys.new(search_in(hostfiles(options), host), host, self, options)
59
- end
51
+ class <<self
52
+ # Searches all known host files (see KnownHosts.hostfiles) for all keys
53
+ # of the given host. Returns an enumerable of keys found.
54
+ def search_for(host, options={})
55
+ HostKeys.new(search_in(hostfiles(options), host, options), host, self, options)
56
+ end
60
57
 
61
- # Search for all known keys for the given host, in every file given in
62
- # the +files+ array. Returns the list of keys.
63
- def search_in(files, host)
64
- files.map { |file| KnownHosts.new(file).keys_for(host) }.flatten
65
- end
58
+ # Search for all known keys for the given host, in every file given in
59
+ # the +files+ array. Returns the list of keys.
60
+ def search_in(files, host, options = {})
61
+ files.flat_map { |file| KnownHosts.new(file).keys_for(host, options) }
62
+ end
66
63
 
67
- # Looks in the given +options+ hash for the :user_known_hosts_file and
68
- # :global_known_hosts_file keys, and returns an array of all known
69
- # hosts files. If the :user_known_hosts_file key is not set, the
70
- # default is returned (~/.ssh/known_hosts and ~/.ssh/known_hosts2). If
71
- # :global_known_hosts_file is not set, the default is used
72
- # (/etc/ssh/ssh_known_hosts and /etc/ssh/ssh_known_hosts2).
73
- #
74
- # If you only want the user known host files, you can pass :user as
75
- # the second option.
76
- def hostfiles(options, which=:all)
77
- files = []
64
+ # Looks in the given +options+ hash for the :user_known_hosts_file and
65
+ # :global_known_hosts_file keys, and returns an array of all known
66
+ # hosts files. If the :user_known_hosts_file key is not set, the
67
+ # default is returned (~/.ssh/known_hosts and ~/.ssh/known_hosts2). If
68
+ # :global_known_hosts_file is not set, the default is used
69
+ # (/etc/ssh/ssh_known_hosts and /etc/ssh/ssh_known_hosts2).
70
+ #
71
+ # If you only want the user known host files, you can pass :user as
72
+ # the second option.
73
+ def hostfiles(options, which=:all)
74
+ files = []
75
+
76
+ files += Array(options[:user_known_hosts_file] || %w[~/.ssh/known_hosts ~/.ssh/known_hosts2]) if which == :all || which == :user
77
+
78
+ if which == :all || which == :global
79
+ files += Array(options[:global_known_hosts_file] || %w[/etc/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts2])
80
+ end
78
81
 
79
- if which == :all || which == :user
80
- files += Array(options[:user_known_hosts_file] || %w(~/.ssh/known_hosts ~/.ssh/known_hosts2))
82
+ return files
81
83
  end
82
84
 
83
- if which == :all || which == :global
84
- files += Array(options[:global_known_hosts_file] || %w(/etc/ssh/ssh_known_hosts /etc/ssh/ssh_known_hosts2))
85
+ # Looks in all user known host files (see KnownHosts.hostfiles) and tries to
86
+ # add an entry for the given host and key to the first file it is able
87
+ # to.
88
+ def add(host, key, options={})
89
+ hostfiles(options, :user).each do |file|
90
+ begin
91
+ KnownHosts.new(file).add(host, key)
92
+ return
93
+ rescue SystemCallError
94
+ # try the next hostfile
95
+ end
96
+ end
85
97
  end
98
+ end
99
+
100
+ # The host-key file name that this KnownHosts instance will use to search
101
+ # for keys.
102
+ attr_reader :source
86
103
 
87
- return files
104
+ # Instantiate a new KnownHosts instance that will search the given known-hosts
105
+ # file. The path is expanded file File.expand_path.
106
+ def initialize(source)
107
+ @source = File.expand_path(source)
88
108
  end
89
109
 
90
- # Looks in all user known host files (see KnownHosts.hostfiles) and tries to
91
- # add an entry for the given host and key to the first file it is able
92
- # to.
93
- def add(host, key, options={})
94
- hostfiles(options, :user).each do |file|
95
- begin
96
- KnownHosts.new(file).add(host, key)
97
- return
98
- rescue SystemCallError
99
- # try the next hostfile
110
+ # Returns an array of all keys that are known to be associatd with the
111
+ # given host. The +host+ parameter is either the domain name or ip address
112
+ # of the host, or both (comma-separated). Additionally, if a non-standard
113
+ # port is being used, it may be specified by putting the host (or ip, or
114
+ # both) in square brackets, and appending the port outside the brackets
115
+ # after a colon. Possible formats for +host+, then, are;
116
+ #
117
+ # "net.ssh.test"
118
+ # "1.2.3.4"
119
+ # "net.ssh.test,1.2.3.4"
120
+ # "[net.ssh.test]:5555"
121
+ # "[1,2,3,4]:5555"
122
+ # "[net.ssh.test]:5555,[1.2.3.4]:5555
123
+ def keys_for(host, options = {})
124
+ keys = []
125
+ return keys unless File.readable?(source)
126
+
127
+ entries = host.split(/,/)
128
+ host_name = entries[0]
129
+ host_ip = entries[1]
130
+
131
+ File.open(source) do |file|
132
+ file.each_line do |line|
133
+ hosts, type, key_content = line.split(' ')
134
+ # Skip empty line or one that is commented
135
+ next if hosts.nil? || hosts.start_with?('#')
136
+
137
+ hostlist = hosts.split(',')
138
+
139
+ next unless SUPPORTED_TYPE.include?(type)
140
+
141
+ found = hostlist.any? { |pattern| match(host_name, pattern) } || known_host_hash?(hostlist, entries)
142
+ next unless found
143
+
144
+ found = hostlist.include?(host_ip) if options[:check_host_ip] && entries.size > 1 && hostlist.size > 1
145
+ next unless found
146
+
147
+ blob = key_content.unpack("m*").first
148
+ keys << Net::SSH::Buffer.new(blob).read_key
100
149
  end
101
150
  end
102
- end
103
- end
104
-
105
- # The host-key file name that this KnownHosts instance will use to search
106
- # for keys.
107
- attr_reader :source
108
151
 
109
- # Instantiate a new KnownHosts instance that will search the given known-hosts
110
- # file. The path is expanded file File.expand_path.
111
- def initialize(source)
112
- @source = File.expand_path(source)
113
- end
152
+ keys
153
+ end
114
154
 
115
- # Returns an array of all keys that are known to be associatd with the
116
- # given host. The +host+ parameter is either the domain name or ip address
117
- # of the host, or both (comma-separated). Additionally, if a non-standard
118
- # port is being used, it may be specified by putting the host (or ip, or
119
- # both) in square brackets, and appending the port outside the brackets
120
- # after a colon. Possible formats for +host+, then, are;
121
- #
122
- # "net.ssh.test"
123
- # "1.2.3.4"
124
- # "net.ssh.test,1.2.3.4"
125
- # "[net.ssh.test]:5555"
126
- # "[1,2,3,4]:5555"
127
- # "[net.ssh.test]:5555,[1.2.3.4]:5555
128
- def keys_for(host)
129
- keys = []
130
- return keys unless File.readable?(source)
131
-
132
- entries = host.split(/,/)
133
-
134
- File.open(source) do |file|
135
- scanner = StringScanner.new("")
136
- file.each_line do |line|
137
- scanner.string = line
138
-
139
- scanner.skip(/\s*/)
140
- next if scanner.match?(/$|#/)
141
-
142
- hostlist = scanner.scan(/\S+/).split(/,/)
143
- found = entries.all? { |entry| hostlist.include?(entry) } ||
144
- known_host_hash?(hostlist, entries, scanner)
145
- next unless found
146
-
147
- scanner.skip(/\s*/)
148
- type = scanner.scan(/\S+/)
149
-
150
- next unless SUPPORTED_TYPE.include?(type)
151
-
152
- scanner.skip(/\s*/)
153
- blob = scanner.rest.unpack("m*").first
154
- keys << Net::SSH::Buffer.new(blob).read_key
155
+ def match(host, pattern)
156
+ if pattern.include?('*') || pattern.include?('?')
157
+ # see man 8 sshd for pattern details
158
+ pattern_regexp = pattern.split('*').map do |x|
159
+ x.split('?').map do |y|
160
+ Regexp.escape(y)
161
+ end.join('.')
162
+ end.join('[^.]*')
163
+
164
+ host =~ Regexp.new("\\A#{pattern_regexp}\\z")
165
+ else
166
+ host == pattern
155
167
  end
156
168
  end
157
169
 
158
- keys
159
- end
160
-
161
- # Indicates whether one of the entries matches an hostname that has been
162
- # stored as a HMAC-SHA1 hash in the known hosts.
163
- def known_host_hash?(hostlist, entries, scanner)
164
- if hostlist.size == 1 && hostlist.first =~ /\A\|1(\|.+){2}\z/
165
- chunks = hostlist.first.split(/\|/)
166
- salt = Base64.decode64(chunks[2])
167
- digest = OpenSSL::Digest.new('sha1')
168
- entries.each do |entry|
169
- hmac = OpenSSL::HMAC.digest(digest, salt, entry)
170
- return true if Base64.encode64(hmac).chomp == chunks[3]
170
+ # Indicates whether one of the entries matches an hostname that has been
171
+ # stored as a HMAC-SHA1 hash in the known hosts.
172
+ def known_host_hash?(hostlist, entries)
173
+ if hostlist.size == 1 && hostlist.first =~ /\A\|1(\|.+){2}\z/
174
+ chunks = hostlist.first.split(/\|/)
175
+ salt = Base64.decode64(chunks[2])
176
+ digest = OpenSSL::Digest.new('sha1')
177
+ entries.each do |entry|
178
+ hmac = OpenSSL::HMAC.digest(digest, salt, entry)
179
+ return true if Base64.encode64(hmac).chomp == chunks[3]
180
+ end
171
181
  end
182
+ false
172
183
  end
173
- false
174
- end
175
184
 
176
- # Tries to append an entry to the current source file for the given host
177
- # and key. If it is unable to (because the file is not writable, for
178
- # instance), an exception will be raised.
179
- def add(host, key)
180
- File.open(source, "a") do |file|
181
- blob = [Net::SSH::Buffer.from(:key, key).to_s].pack("m*").gsub(/\s/, "")
182
- file.puts "#{host} #{key.ssh_type} #{blob}"
185
+ # Tries to append an entry to the current source file for the given host
186
+ # and key. If it is unable to (because the file is not writable, for
187
+ # instance), an exception will be raised.
188
+ def add(host, key)
189
+ File.open(source, "a") do |file|
190
+ blob = [Net::SSH::Buffer.from(:key, key).to_s].pack("m*").gsub(/\s/, "")
191
+ file.puts "#{host} #{key.ssh_type} #{blob}"
192
+ end
183
193
  end
184
194
  end
185
-
186
195
  end
187
- end; end
196
+ end