net-ssh 4.1.0 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
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