sshkey 1.9.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e557b4605e1f00d77de9e7b939f79a5ba9601cdf
4
- data.tar.gz: 6764a84f581a544e0adea8958731eb01bd35a550
2
+ SHA256:
3
+ metadata.gz: 9e933102cbc9909bfcb4564475ef8ea53427869b58c424405a588c954f3f737c
4
+ data.tar.gz: 415a21ff41613ba75ce07dab568f8560c9b3b83b0b3e7cac015f4238f20420b0
5
5
  SHA512:
6
- metadata.gz: dd96a9bacc99265e0f211b40a4c0d4a860ed4f6e5ffb7197277f5fdf11800776f6dbf687288b4c2b4bf3829e02273b2dcaa7b5c8fec662fda8b695b6f5861c5c
7
- data.tar.gz: 8d68590c6f42a5d3868eaa0c39a8880c8c2666c5f63ac9d53df9cee0cba7f7827a3f3f3343166adecdca88e05de7599b0c7c163327ca12f72f3f04f2a0e0b965
6
+ metadata.gz: 5867e989c0296a84807b9cf52e2d0512f3083c12f07a82fb8f9bca1bf475e38fd658132d5cbd91df03cee83114665c325f1a58e4c195b037f08f8feb648c188f
7
+ data.tar.gz: b314c5762fbe08f6e95ba8d18d01e6a51ef25656cd3797de8e3d2065d1066768656ac8c1ac030379672b2ed27d3672e5cbbcdc6d499fd90f7e35ba4b356f25b2
@@ -0,0 +1,25 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ test:
14
+ runs-on: ubuntu-latest
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ ruby: ["2.5", "2.6", "2.7", "3.0", "3.1", "3.2", "3.3", jruby-9.3, jruby-9.4]
19
+ steps:
20
+ - uses: actions/checkout@v3
21
+ - uses: ruby/setup-ruby@v1
22
+ with:
23
+ bundler-cache: true
24
+ ruby-version: ${{ matrix.ruby }}
25
+ - run: bundle exec rake
data/Gemfile CHANGED
@@ -1,7 +1,5 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem "jruby-openssl", ">= 0.8.2", :platforms => :jruby
4
- gem "rubysl", "~> 2.0.15", :platforms => :rbx
5
- gem "rubysl-test-unit", "~> 2.0.3", :platforms => :rbx
3
+ gem "jruby-openssl", ">= 0.8.2", platform: :jruby
6
4
 
7
5
  gemspec
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011-2016 James Miller
1
+ Copyright (c) 2011-2023 James Miller
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,22 +1,20 @@
1
1
  # SSHKey
2
2
 
3
- Generate private and public SSH keys (RSA and DSA supported) using pure Ruby.
4
-
5
- [![Build Status](https://secure.travis-ci.org/bensie/sshkey.svg?branch=master)](http://travis-ci.org/bensie/sshkey)
3
+ Generate private and public SSH keys (RSA, DSA, and ECDSA supported) using pure Ruby.
6
4
 
7
5
  ## Requirements
8
6
 
9
- Tested / supported on CRuby 1.9.3+ and JRuby.
7
+ Tested / supported on CRuby 2.5+ and JRuby.
10
8
 
11
9
  ## Installation
12
10
 
13
- gem install sshkey
11
+ gem install sshkey
14
12
 
15
13
  ## Usage
16
14
 
17
15
  ### Generate a new key
18
16
 
19
- When generating a new keypair the default key type is 2048-bit RSA, but you can supply the `type` (RSA or DSA) and `bits` in the options.
17
+ When generating a new keypair the default key type is 2048-bit RSA, but you can supply the `type` (RSA or DSA or ECDSA) and `bits` in the options.
20
18
  You can also (optionally) supply a `comment` or `passphrase`.
21
19
 
22
20
  ```ruby
@@ -32,17 +30,18 @@ k = SSHKey.generate(
32
30
 
33
31
  ### Use your existing key
34
32
 
35
- Return an SSHKey object from an existing RSA or DSA private key (provided as a string).
33
+ Return an SSHKey object from an existing RSA or DSA or ECDSA private key (provided as a string).
36
34
 
37
35
  ```ruby
38
- k = SSHKey.new(File.read("~/.ssh/id_rsa"), comment: "foo@bar.com")
36
+ f = File.read(File.expand_path("~/.ssh/id_rsa"))
37
+ k = SSHKey.new(f, comment: "foo@bar.com")
39
38
  ```
40
39
 
41
40
  ### The SSHKey object
42
41
 
43
42
  #### Private and public keys
44
43
 
45
- Fetch the private and public keys as strings. Note that the `public_key` is the RSA or DSA public key, not an SSH public key.
44
+ Fetch the private and public keys as strings. Note that the `public_key` is the RSA or DSA or ECDSA public key, not an SSH public key.
46
45
 
47
46
  ```ruby
48
47
  k.private_key
@@ -90,6 +89,7 @@ k.ssh_public_key
90
89
  k.ssh2_public_key
91
90
  # => "---- BEGIN SSH2 PUBLIC KEY ----\nComment: me@me.com\nAAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+n\nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5\nXsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoA\nv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I\n9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVIC\nWtKbqW263HT5LvSxwKorR7\n---- END SSH2 PUBLIC KEY ----"
92
91
  ```
92
+
93
93
  #### Bit length
94
94
 
95
95
  Determine the strength of the key in bits as an integer.
@@ -159,7 +159,7 @@ puts k.randomart
159
159
 
160
160
  #### Original OpenSSL key object
161
161
 
162
- Return the original [OpenSSL::PKey::RSA](http://www.ruby-doc.org/stdlib/libdoc/openssl/rdoc/classes/OpenSSL/PKey/RSA.html) or [OpenSSL::PKey::DSA](http://www.ruby-doc.org/stdlib/libdoc/openssl/rdoc/classes/OpenSSL/PKey/DSA.html) object.
162
+ Return the original [OpenSSL::PKey::RSA](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/PKey/RSA.html) or [OpenSSL::PKey::DSA](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/PKey/DSA.html) or [OpenSSL::PKey::EC](https://ruby-doc.org/3.2.2/exts/openssl/OpenSSL/PKey/EC.html)object.
163
163
 
164
164
  ```ruby
165
165
  k.key_object
@@ -179,7 +179,7 @@ SSHKey.valid_ssh_public_key? "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE
179
179
 
180
180
  #### Bit length
181
181
 
182
- Determine the strength of the key in bits as an integer. Returns `SSHKey::PublicKeyError` if bits cannot be determined.
182
+ Determine the strength of the key in bits as an integer. Returns `SSHKey::PublicKeyError` if bits cannot be determined.
183
183
 
184
184
  ```ruby
185
185
  SSHKey.ssh_public_key_bits "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5XsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoAv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVICWtKbqW263HT5LvSxwKorR7"
@@ -202,7 +202,7 @@ SSHKey.sha256_fingerprint "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/
202
202
 
203
203
  #### Convert to SSH2 Public Key
204
204
 
205
- Convert an existing SSH Public Key into an SSH2 Public key. Returns `SSHKey::PublicKeyError` if a valid key cannot be generated.
205
+ Convert an existing SSH Public Key into an SSH2 Public key. Returns `SSHKey::PublicKeyError` if a valid key cannot be generated.
206
206
 
207
207
  ```ruby
208
208
  SSHKey.ssh_public_key_to_ssh2_public_key "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9HuXvYJPtQE/o/7TYi63yAopsrJ6TP+lDGdyQ+nVVp+5ojAIy9h8/h99UlNxjkiFT2YhI3Fl/pgNDRO4PVo6tlgb3CwiAZjSdeE5RnF79Dkj5XsM4j+FLMoXtbRw0K9ok9RKjz6ygIs1JDmaOdXexFnq4nAYU3fSLUa6WoccqTHe8bFuJoAv1gbnx09Js8YcVMD96mpTJ3V/MK5YfIv10dbtrDhGug3IS1V2J+0BB9orbQja554N+4S0I9rFBgVCpvPmQqddDHd/AdGkLv/zjEfGytjnvp68bEfDinkQkPfuxw01yd5MbcvLv39VVICWtKbqW263HT5LvSxwKorR7 me@me.com"
@@ -211,4 +211,4 @@ SSHKey.ssh_public_key_to_ssh2_public_key "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ
211
211
 
212
212
  ## Copyright
213
213
 
214
- Copyright (c) 2011-2016 James Miller
214
+ Copyright (c) 2011-2023 James Miller
@@ -1,3 +1,3 @@
1
1
  class SSHKey
2
- VERSION = "1.9.0"
2
+ VERSION = "3.0.0"
3
3
  end
data/lib/sshkey.rb CHANGED
@@ -1,10 +1,37 @@
1
- $:.unshift File.dirname(__FILE__)
2
-
3
1
  require 'openssl'
4
2
  require 'base64'
5
3
  require 'digest/md5'
6
4
  require 'digest/sha1'
7
- require 'sshkey/exception'
5
+ require 'digest/sha2'
6
+
7
+ def jruby_not_implemented(msg)
8
+ raise NotImplementedError.new "jruby-openssl #{JOpenSSL::VERSION}: #{msg}" if RUBY_PLATFORM == "java"
9
+ end
10
+
11
+ # Monkey patch OpenSSL::PKey::EC to provide convenience methods usable in this gem
12
+ class OpenSSL::PKey::EC
13
+ def identifier
14
+ # NOTE: Unable to find these constants within OpenSSL, so hardcode them here.
15
+ # Analogous to net-ssh OpenSSL::PKey::EC::CurveNameAliasInv
16
+ # https://github.com/net-ssh/net-ssh/blob/master/lib/net/ssh/transport/openssl.rb#L147-L151
17
+ case group.curve_name
18
+ when "prime256v1" then "nistp256" # https://stackoverflow.com/a/41953717
19
+ when "secp256r1" then "nistp256" # JRuby
20
+ when "secp384r1" then "nistp384"
21
+ when "secp521r1" then "nistp521"
22
+ else
23
+ raise "Unknown curve name: #{public_key.group.curve_name}"
24
+ end
25
+ end
26
+
27
+ def q
28
+ # jruby-openssl does not currently support to_octet_string
29
+ # https://github.com/jruby/jruby-openssl/issues/226
30
+ jruby_not_implemented("to_octet_string is not implemented")
31
+
32
+ public_key.to_octet_string(group.point_conversion_form)
33
+ end
34
+ end
8
35
 
9
36
  class SSHKey
10
37
  SSH_TYPES = {
@@ -15,7 +42,30 @@ class SSHKey
15
42
  "ecdsa-sha2-nistp384" => "ecdsa",
16
43
  "ecdsa-sha2-nistp521" => "ecdsa",
17
44
  }
18
- SSH_CONVERSION = {"rsa" => ["e", "n"], "dsa" => ["p", "q", "g", "pub_key"]}
45
+
46
+ SSHFP_TYPES = {
47
+ "rsa" => 1,
48
+ "dsa" => 2,
49
+ "ecdsa" => 3,
50
+ "ed25519" => 4,
51
+ }
52
+
53
+ ECDSA_CURVES = {
54
+ 256 => "prime256v1", # https://stackoverflow.com/a/41953717
55
+ 384 => "secp384r1",
56
+ 521 => "secp521r1",
57
+ }
58
+
59
+ VALID_BITS = {
60
+ "ecdsa" => ECDSA_CURVES.keys,
61
+ }
62
+
63
+ # Accessor methods are defined in:
64
+ # - RSA: https://github.com/ruby/openssl/blob/master/ext/openssl/ossl_pkey_rsa.c
65
+ # - DSA: https://github.com/ruby/openssl/blob/master/ext/openssl/ossl_pkey_dsa.c
66
+ # - ECDSA: monkey patch OpenSSL::PKey::EC above
67
+ SSH_CONVERSION = {"rsa" => ["e", "n"], "dsa" => ["p", "q", "g", "pub_key"], "ecdsa" => ["identifier", "q"]}
68
+
19
69
  SSH2_LINE_LENGTH = 70 # +1 (for line wrap '/' character) must be <= 72
20
70
 
21
71
  class << self
@@ -35,17 +85,44 @@ class SSHKey
35
85
  type = options[:type] || "rsa"
36
86
 
37
87
  # JRuby modulus size must range from 512 to 1024
38
- default_bits = type == "rsa" ? 2048 : 1024
88
+ case type
89
+ when "rsa" then default_bits = 2048
90
+ when "ecdsa" then default_bits = 256
91
+ else
92
+ default_bits = 1024
93
+ end
39
94
 
40
95
  bits = options[:bits] || default_bits
41
- cipher = OpenSSL::Cipher::Cipher.new("AES-128-CBC") if options[:passphrase]
96
+ cipher = OpenSSL::Cipher.new("AES-128-CBC") if options[:passphrase]
97
+
98
+ raise "Bits must either: #{VALID_BITS[type.downcase].join(', ')}" unless VALID_BITS[type.downcase].nil? || VALID_BITS[type.downcase].include?(bits)
42
99
 
43
100
  case type.downcase
44
- when "rsa" then new(OpenSSL::PKey::RSA.generate(bits).to_pem(cipher, options[:passphrase]), options)
45
- when "dsa" then new(OpenSSL::PKey::DSA.generate(bits).to_pem(cipher, options[:passphrase]), options)
101
+ when "rsa"
102
+ key_object = OpenSSL::PKey::RSA.generate(bits)
103
+
104
+ when "dsa"
105
+ key_object = OpenSSL::PKey::DSA.generate(bits)
106
+
107
+ when "ecdsa"
108
+ # jruby-openssl OpenSSL::PKey::EC support isn't complete
109
+ # https://github.com/jruby/jruby-openssl/issues/189
110
+ jruby_not_implemented("OpenSSL::PKey::EC is not fully implemented")
111
+
112
+ if OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30000000
113
+ # https://github.com/ruby/openssl/pull/480
114
+ key_object = OpenSSL::PKey::EC.generate(ECDSA_CURVES[bits])
115
+ else
116
+ key_pkey = OpenSSL::PKey::EC.new(ECDSA_CURVES[bits])
117
+ key_object = key_pkey.generate_key
118
+ end
119
+
46
120
  else
47
121
  raise "Unknown key type: #{type}"
48
122
  end
123
+
124
+ key_pem = key_object.to_pem(cipher, options[:passphrase])
125
+ new(key_pem, options)
49
126
  end
50
127
 
51
128
  # Validate an existing SSH public key
@@ -62,7 +139,7 @@ class SSHKey
62
139
  when "ssh-rsa", "ssh-dss"
63
140
  sections.size == SSH_CONVERSION[SSH_TYPES[ssh_type]].size
64
141
  when "ssh-ed25519"
65
- sections.size == 1 && sections[0].num_bytes == 32 # https://tools.ietf.org/id/draft-bjh21-ssh-ed25519-00.html#rfc.section.4
142
+ sections.size == 1 # https://tools.ietf.org/id/draft-bjh21-ssh-ed25519-00.html#rfc.section.4
66
143
  when "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521"
67
144
  sections.size == 2 # https://tools.ietf.org/html/rfc5656#section-3.1
68
145
  else
@@ -78,9 +155,27 @@ class SSHKey
78
155
  #
79
156
  # ==== Parameters
80
157
  # * ssh_public_key<~String> - "ssh-rsa AAAAB3NzaC1yc2EA...."
158
+ # * ssh_public_key<~String> - "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY...."
81
159
  #
82
160
  def ssh_public_key_bits(ssh_public_key)
83
- unpacked_byte_array( *parse_ssh_public_key(ssh_public_key) ).last.num_bytes * 8
161
+ ssh_type, encoded_key = parse_ssh_public_key(ssh_public_key)
162
+ sections = unpacked_byte_array(ssh_type, encoded_key)
163
+
164
+ case ssh_type
165
+ when "ssh-rsa", "ssh-dss", "ssh-ed25519"
166
+ sections.last.num_bytes * 8
167
+
168
+ when "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521"
169
+ raise PublicKeyError, "invalid ECDSA key" unless sections.count == 2
170
+
171
+ # https://tools.ietf.org/html/rfc5656#section-3.1
172
+ identifier = sections[0].to_s(2)
173
+ q = sections[1].to_s(2)
174
+ ecdsa_bits(ssh_type, identifier, q)
175
+
176
+ else
177
+ raise PublicKeyError, "unsupported key type #{ssh_type}"
178
+ end
84
179
  end
85
180
 
86
181
  # Fingerprints
@@ -115,6 +210,16 @@ class SSHKey
115
210
  end
116
211
  end
117
212
 
213
+ # SSHFP records for the given SSH key
214
+ def sshfp(hostname, key)
215
+ if key.match(/PRIVATE/)
216
+ new(key).sshfp hostname
217
+ else
218
+ type, encoded_key = parse_ssh_public_key(key)
219
+ format_sshfp_record(hostname, SSH_TYPES[type], Base64.decode64(encoded_key))
220
+ end
221
+ end
222
+
118
223
  # Convert an existing SSH public key to SSH2 (RFC4716) public key
119
224
  #
120
225
  # ==== Parameters
@@ -124,7 +229,7 @@ class SSHKey
124
229
  def ssh_public_key_to_ssh2_public_key(ssh_public_key, headers = nil)
125
230
  raise PublicKeyError, "invalid ssh public key" unless SSHKey.valid_ssh_public_key?(ssh_public_key)
126
231
 
127
- source_format, source_key = parse_ssh_public_key(ssh_public_key)
232
+ _source_format, source_key = parse_ssh_public_key(ssh_public_key)
128
233
 
129
234
  # Add a 'Comment' Header Field unless others are explicitly passed in
130
235
  if source_comment = ssh_public_key.split(source_key)[1]
@@ -138,6 +243,13 @@ class SSHKey
138
243
  ssh2_key << "\n---- END SSH2 PUBLIC KEY ----"
139
244
  end
140
245
 
246
+ def format_sshfp_record(hostname, type, key)
247
+ [[Digest::SHA1, 1], [Digest::SHA256, 2]].map { |f, num|
248
+ fpr = f.hexdigest(key)
249
+ "#{hostname} IN SSHFP #{SSHFP_TYPES[type]} #{num} #{fpr}"
250
+ }.join("\n")
251
+ end
252
+
141
253
  private
142
254
 
143
255
  def unpacked_byte_array(ssh_type, encoded_key)
@@ -149,19 +261,71 @@ class SSHKey
149
261
  raise PublicKeyError, "validation error"
150
262
  end
151
263
 
264
+ byte_count = 0
152
265
  data = []
153
266
  until decoded.empty?
154
267
  front = decoded.slice!(0,4)
155
268
  size = front.unpack("N").first
156
269
  segment = decoded.slice!(0, size)
270
+ byte_count += segment.length
157
271
  unless front.length == 4 && segment.length == size
158
272
  raise PublicKeyError, "byte array too short"
159
273
  end
160
274
  data << OpenSSL::BN.new(segment, 2)
161
275
  end
276
+
277
+
278
+ if ssh_type == "ssh-ed25519"
279
+ unless byte_count == 32
280
+ raise PublicKeyError, "validation error, ed25519 key length not OK"
281
+ end
282
+ end
283
+
162
284
  return data
163
285
  end
164
286
 
287
+ def ecdsa_bits(ssh_type, identifier, q)
288
+ raise PublicKeyError, "invalid ssh type" unless ssh_type == "ecdsa-sha2-#{identifier}"
289
+
290
+ len_q = q.length
291
+
292
+ compression_octet = q.slice(0, 1)
293
+ if compression_octet == "\x04"
294
+ # Point compression is off
295
+ # Summary from https://www.secg.org/sec1-v2.pdf "2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion"
296
+ # - the leftmost octet indicates that point compression is off
297
+ # (first octet 0x04 as specified in "3.3. Output M = 04 base 16 ‖ X ‖ Y.")
298
+ # - the remainder of the octet string contains the x-coordinate followed by the y-coordinate.
299
+ len_x = (len_q - 1) / 2
300
+
301
+ else
302
+ # Point compression is on
303
+ # Summary from https://www.secg.org/sec1-v2.pdf "2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion"
304
+ # - the compressed y-coordinate is recovered from the leftmost octet
305
+ # - the x-coordinate is recovered from the remainder of the octet string
306
+ raise PublicKeyError, "invalid compression octet" unless compression_octet == "\x02" || compression_octet == "\x03"
307
+ len_x = len_q - 1
308
+ end
309
+
310
+ # https://www.secg.org/sec2-v2.pdf "2.1 Properties of Elliptic Curve Domain Parameters over Fp" defines
311
+ # five discrete bit lengths: 192, 224, 256, 384, 521
312
+ # These bit lengths can be ascertained from the length of the packed x-coordinate.
313
+ # Alternatively, these bit lengths can be derived from their associated prime constants using Math.log2(prime).ceil
314
+ # against the prime constants defined in https://www.secg.org/sec2-v2.pdf
315
+ case len_x
316
+ when 24 then bits = 192
317
+ when 28 then bits = 224
318
+ when 32 then bits = 256
319
+ when 48 then bits = 384
320
+ when 66 then bits = 521
321
+ else
322
+ raise PublicKeyError, "invalid x-coordinate length #{len_x}"
323
+ end
324
+
325
+ raise PublicKeyError, "invalid identifier #{identifier}" unless identifier =~ /#{bits}/
326
+ return bits
327
+ end
328
+
165
329
  def decoded_key(key)
166
330
  Base64.decode64(parse_ssh_public_key(key).last)
167
331
  end
@@ -171,6 +335,10 @@ class SSHKey
171
335
  end
172
336
 
173
337
  def parse_ssh_public_key(public_key)
338
+ # lines starting with a '#' and empty lines are ignored as comments (as in ssh AuthorizedKeysFile)
339
+ public_key = public_key.gsub(/^#.*$/, '')
340
+ public_key = public_key.strip # leading and trailing whitespaces wiped out
341
+
174
342
  raise PublicKeyError, "newlines are not permitted between key data" if public_key =~ /\n(?!$)/
175
343
 
176
344
  parsed = public_key.split(" ")
@@ -195,13 +363,13 @@ class SSHKey
195
363
  end
196
364
  end
197
365
 
198
- attr_reader :key_object, :type
366
+ attr_reader :key_object, :type, :typestr
199
367
  attr_accessor :passphrase, :comment
200
368
 
201
369
  # Create a new SSHKey object
202
370
  #
203
371
  # ==== Parameters
204
- # * private_key - Existing RSA or DSA private key
372
+ # * private_key - Existing RSA or DSA or ECDSA private key
205
373
  # * options<~Hash>
206
374
  # * :comment<~String> - Comment to use for the public key, defaults to ""
207
375
  # * :passphrase<~String> - If the key is encrypted, supply the passphrase
@@ -211,19 +379,41 @@ class SSHKey
211
379
  @passphrase = options[:passphrase]
212
380
  @comment = options[:comment] || ""
213
381
  self.directives = options[:directives] || []
382
+
214
383
  begin
215
384
  @key_object = OpenSSL::PKey::RSA.new(private_key, passphrase)
216
385
  @type = "rsa"
217
- rescue
386
+ @typestr = "ssh-rsa"
387
+ rescue OpenSSL::PKey::RSAError
388
+ @type = nil
389
+ end
390
+
391
+ return if @type
392
+
393
+ begin
218
394
  @key_object = OpenSSL::PKey::DSA.new(private_key, passphrase)
219
395
  @type = "dsa"
396
+ @typestr = "ssh-dss"
397
+ rescue OpenSSL::PKey::DSAError
398
+ @type = nil
220
399
  end
400
+
401
+ return if @type
402
+
403
+ @key_object = OpenSSL::PKey::EC.new(private_key, passphrase)
404
+ @type = "ecdsa"
405
+ bits = ECDSA_CURVES.invert[@key_object.group.curve_name]
406
+ @typestr = "ecdsa-sha2-nistp#{bits}"
221
407
  end
222
408
 
223
- # Fetch the RSA/DSA private key
409
+ # Fetch the private key (PEM format)
224
410
  #
225
411
  # rsa_private_key and dsa_private_key are aliased for backward compatibility
226
412
  def private_key
413
+ # jruby-openssl OpenSSL::PKey::EC support isn't complete
414
+ # https://github.com/jruby/jruby-openssl/issues/189
415
+ jruby_not_implemented("OpenSSL::PKey::EC is not fully implemented") if type == "ecdsa"
416
+
227
417
  key_object.to_pem
228
418
  end
229
419
  alias_method :rsa_private_key, :private_key
@@ -234,21 +424,76 @@ class SSHKey
234
424
  # If no passphrase is set, returns the unencrypted private key
235
425
  def encrypted_private_key
236
426
  return private_key unless passphrase
237
- key_object.to_pem(OpenSSL::Cipher::Cipher.new("AES-128-CBC"), passphrase)
427
+ key_object.to_pem(OpenSSL::Cipher.new("AES-128-CBC"), passphrase)
238
428
  end
239
429
 
240
- # Fetch the RSA/DSA public key
430
+ # Fetch the public key (PEM format)
241
431
  #
242
432
  # rsa_public_key and dsa_public_key are aliased for backward compatibility
243
433
  def public_key
244
- key_object.public_key.to_pem
434
+ public_key_object.to_pem
245
435
  end
246
436
  alias_method :rsa_public_key, :public_key
247
437
  alias_method :dsa_public_key, :public_key
248
438
 
439
+ def public_key_object
440
+ if type == "ecdsa"
441
+ return nil unless key_object
442
+ return nil unless key_object.group
443
+
444
+ if OpenSSL::OPENSSL_VERSION_NUMBER >= 0x30000000 && RUBY_PLATFORM != "java"
445
+
446
+ # jruby-openssl does not currently support point_conversion_form
447
+ # (futureproofing for if/when JRuby requires this technique to determine public key)
448
+ jruby_not_implemented("point_conversion_form is not implemented")
449
+
450
+ # Avoid "OpenSSL::PKey::PKeyError: pkeys are immutable on OpenSSL 3.0"
451
+ # https://github.com/ruby/openssl/blob/master/History.md#version-300
452
+ # https://github.com/ruby/openssl/issues/498
453
+ # https://github.com/net-ssh/net-ssh/commit/4de6831dea4e922bf3052192eec143af015a3486
454
+ # https://github.com/ClearlyClaire/cose-ruby/commit/28ee497fa7d9d49e72d5a5e97a567c0b58fdd822
455
+
456
+ curve_name = key_object.group.curve_name
457
+ return nil unless curve_name
458
+
459
+ # Map to different curve_name for JRuby
460
+ # (futureproofing for if/when JRuby requires this technique to determine public key)
461
+ # https://github.com/jwt/ruby-jwt/issues/362#issuecomment-722938409
462
+ curve_name = "prime256v1" if curve_name == "secp256r1" && RUBY_PLATFORM == "java"
463
+
464
+ # Construct public key OpenSSL::PKey::EC from OpenSSL::PKey::EC::Point
465
+ public_key_point = key_object.public_key # => OpenSSL::PKey::EC::Point
466
+ return nil unless public_key_point
467
+
468
+ asn1 = OpenSSL::ASN1::Sequence(
469
+ [
470
+ OpenSSL::ASN1::Sequence(
471
+ [
472
+ OpenSSL::ASN1::ObjectId("id-ecPublicKey"),
473
+ OpenSSL::ASN1::ObjectId(curve_name)
474
+ ]
475
+ ),
476
+ OpenSSL::ASN1::BitString(public_key_point.to_octet_string(key_object.group.point_conversion_form))
477
+ ]
478
+ )
479
+
480
+ pub = OpenSSL::PKey::EC.new(asn1.to_der)
481
+ pub
482
+
483
+ else
484
+ pub = OpenSSL::PKey::EC.new(key_object.group)
485
+ pub.public_key = key_object.public_key
486
+ pub
487
+ end
488
+
489
+ else
490
+ key_object.public_key
491
+ end
492
+ end
493
+
249
494
  # SSH public key
250
495
  def ssh_public_key
251
- [directives.join(",").strip, SSH_TYPES.invert[type], Base64.encode64(ssh_public_key_conversion).gsub("\n", ""), comment].join(" ").strip
496
+ [directives.join(",").strip, typestr, Base64.encode64(ssh_public_key_conversion).gsub("\n", ""), comment].join(" ").strip
252
497
  end
253
498
 
254
499
  # SSH2 public key (RFC4716)
@@ -291,6 +536,7 @@ class SSHKey
291
536
  #
292
537
  # Generate OpenSSH compatible ASCII art fingerprints
293
538
  # See http://www.opensource.apple.com/source/OpenSSH/OpenSSH-175/openssh/key.c (key_fingerprint_randomart function)
539
+ # or https://mirrors.mit.edu/pub/OpenBSD/OpenSSH/ (sshkey.c fingerprint_randomart function)
294
540
  #
295
541
  # Example:
296
542
  # +--[ RSA 2048]----+
@@ -304,13 +550,23 @@ class SSHKey
304
550
  # | . . |
305
551
  # | Eo. |
306
552
  # +-----------------+
307
- def randomart
553
+ def randomart(dgst_alg = "MD5")
308
554
  fieldsize_x = 17
309
555
  fieldsize_y = 9
310
556
  x = fieldsize_x / 2
311
557
  y = fieldsize_y / 2
312
- raw_digest = Digest::MD5.digest(ssh_public_key_conversion)
313
- num_bytes = raw_digest.bytesize
558
+
559
+ case dgst_alg
560
+ when "MD5" then raw_digest = Digest::MD5.digest(ssh_public_key_conversion)
561
+ when "SHA256" then raw_digest = Digest::SHA2.new(256).digest(ssh_public_key_conversion)
562
+ when "SHA384" then raw_digest = Digest::SHA2.new(384).digest(ssh_public_key_conversion)
563
+ when "SHA512" then raw_digest = Digest::SHA2.new(512).digest(ssh_public_key_conversion)
564
+ else
565
+ raise "Unknown digest algorithm: #{digest}"
566
+ end
567
+
568
+ augmentation_string = " .o+=*BOX@%&#/^SE"
569
+ len = augmentation_string.length - 1
314
570
 
315
571
  field = Array.new(fieldsize_x) { Array.new(fieldsize_y) {0} }
316
572
 
@@ -322,20 +578,27 @@ class SSHKey
322
578
  x = [[x, 0].max, fieldsize_x - 1].min
323
579
  y = [[y, 0].max, fieldsize_y - 1].min
324
580
 
325
- field[x][y] += 1 if (field[x][y] < num_bytes - 2)
581
+ field[x][y] += 1 if (field[x][y] < len - 2)
326
582
 
327
583
  byte >>= 2
328
584
  end
329
585
  end
330
586
 
331
- field[fieldsize_x / 2][fieldsize_y / 2] = num_bytes - 1
332
- field[x][y] = num_bytes
333
- augmentation_string = " .o+=*BOX@%&#/^SE"
334
- output = "+--#{sprintf("[%4s %4u]", type.upcase, bits)}----+\n"
587
+ fieldsize_x_halved = fieldsize_x / 2
588
+ fieldsize_y_halved = fieldsize_y / 2
589
+
590
+ field[fieldsize_x_halved][fieldsize_y_halved] = len - 1
591
+ field[x][y] = len
592
+
593
+ type_name_length_max = 4 # Note: this will need to be extended to accomodate ed25519
594
+ bits_number_length_max = (bits < 1000 ? 3 : 4)
595
+ formatstr = "[%#{type_name_length_max}s %#{bits_number_length_max}u]"
596
+ output = "+--#{sprintf(formatstr, type.upcase, bits)}----+\n"
597
+
335
598
  fieldsize_y.times do |y|
336
599
  output << "|"
337
600
  fieldsize_x.times do |x|
338
- output << augmentation_string[[field[x][y], num_bytes].min]
601
+ output << augmentation_string[[field[x][y], len].min]
339
602
  end
340
603
  output << "|"
341
604
  output << "\n"
@@ -344,6 +607,10 @@ class SSHKey
344
607
  output
345
608
  end
346
609
 
610
+ def sshfp(hostname)
611
+ self.class.format_sshfp_record(hostname, @type, ssh_public_key_conversion)
612
+ end
613
+
347
614
  def directives=(directives)
348
615
  @directives = Array[directives].flatten.compact
349
616
  end
@@ -351,6 +618,26 @@ class SSHKey
351
618
 
352
619
  private
353
620
 
621
+ def self.ssh_public_key_data_dsarsa(val)
622
+ # Get byte-representation of absolute value of val
623
+ data = val.to_s(2)
624
+
625
+ first_byte = data[0,1].unpack("c").first
626
+ if val < 0
627
+ # For negative values, highest bit must be set
628
+ data[0] = [0x80 & first_byte].pack("c")
629
+ elsif first_byte < 0
630
+ # For positive values where highest bit would be set, prefix with \0
631
+ data = "\0" + data
632
+ end
633
+
634
+ data
635
+ end
636
+
637
+ def self.ssh_public_key_data_ecdsa(val)
638
+ val
639
+ end
640
+
354
641
  # SSH Public Key Conversion
355
642
  #
356
643
  # All data type encoding is defined in the section #5 of RFC #4251.
@@ -361,27 +648,23 @@ class SSHKey
361
648
  # For instance, the "ssh-rsa" string is encoded as the following byte array
362
649
  # [0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a']
363
650
  def ssh_public_key_conversion
364
- typestr = SSH_TYPES.invert[type]
365
651
  methods = SSH_CONVERSION[type]
366
- pubkey = key_object.public_key
367
- methods.inject([7].pack("N") + typestr) do |pubkeystr, m|
368
- # Given pubkey.class == OpenSSL::BN, pubkey.to_s(0) returns an MPI
369
- # formatted string (length prefixed bytes). This is not supported by
370
- # JRuby, so we still have to deal with length and data separately.
371
- val = pubkey.send(m)
372
-
373
- # Get byte-representation of absolute value of val
374
- data = val.to_s(2)
375
-
376
- first_byte = data[0,1].unpack("c").first
377
- if val < 0
378
- # For negative values, highest bit must be set
379
- data[0] = [0x80 & first_byte].pack("c")
380
- elsif first_byte < 0
381
- # For positive values where highest bit would be set, prefix with \0
382
- data = "\0" + data
652
+ methods.inject([typestr.length].pack("N") + typestr) do |pubkeystr, m|
653
+ # Given public_key_object.class == OpenSSL::BN, public_key_object.to_s(0)
654
+ # returns an MPI formatted string (length prefixed bytes). This is not
655
+ # supported by JRuby, so we still have to deal with length and data separately.
656
+ val = public_key_object.send(m)
657
+
658
+ case type
659
+ when "dsa","rsa" then data = self.class.ssh_public_key_data_dsarsa(val)
660
+ when "ecdsa" then data = self.class.ssh_public_key_data_ecdsa(val)
661
+ else
662
+ raise "Unknown key type: #{type}"
383
663
  end
664
+
384
665
  pubkeystr + [data.length].pack("N") + data
385
666
  end
386
667
  end
668
+
669
+ class PublicKeyError < StandardError; end
387
670
  end
data/sshkey.gemspec CHANGED
@@ -13,7 +13,10 @@ Gem::Specification.new do |s|
13
13
  s.description = %q{Generate private/public SSH keypairs using pure Ruby}
14
14
  s.licenses = ["MIT"]
15
15
 
16
- s.rubyforge_project = "sshkey"
16
+ # ECDSA requires OpenSSL::PKey::EC::Point#to_octet_string
17
+ # to_octet string was added in Ruby/OpenSSL 2.1.0 https://github.com/ruby/openssl/blob/master/History.md#version-210
18
+ # Ruby 2.5 Updated Ruby/OpenSSL from to 2.1.0 https://github.com/ruby/ruby/blob/v2_5_0/NEWS
19
+ s.required_ruby_version = '>= 2.5'
17
20
 
18
21
  s.files = `git ls-files`.split("\n")
19
22
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
data/test/sshkey_test.rb CHANGED
@@ -2,6 +2,13 @@ require 'test/unit'
2
2
  require 'sshkey'
3
3
 
4
4
  class SSHKeyTest < Test::Unit::TestCase
5
+
6
+ # https://github.com/jruby/jruby-openssl/issues/189
7
+ # https://github.com/jruby/jruby-openssl/issues/226
8
+ def ecdsa_supported?
9
+ RUBY_PLATFORM != "java"
10
+ end
11
+
5
12
  SSH_PRIVATE_KEY1 = <<-EOF
6
13
  -----BEGIN RSA PRIVATE KEY-----
7
14
  MIIEogIBAAKCAQEArfTA/lKVR84IMc9ZzXOCHr8DVtR8hzWuEVHF6KElavRHlk14
@@ -73,34 +80,104 @@ ItmZYXTvJDwLXgq2/iK1fRRcKk2PJEaSuJR7WeNGsJKfWmQ2UbOhqA3wWLDazIZt
73
80
  cMKjFzD0hM4E8qgjHjMvKDE6WgT6SFP+tqx3nnh7pJWwsbGjSMQexpyRAhQLhz0l
74
81
  GzM8qwTcXd06uIZAJdTHIQ==
75
82
  -----END DSA PRIVATE KEY-----
83
+ EOF
84
+
85
+ SSH_PRIVATE_KEY4 = <<-EOF
86
+ -----BEGIN EC PRIVATE KEY-----
87
+ MHcCAQEEIByjVCRawGxEd/L/VblGjnJTJeOgk6vGFYnolYWHg+JkoAoGCCqGSM49
88
+ AwEHoUQDQgAEQOAmNzXT3XN5DQdHBYCgflosVlHd6MUB1n9n6CCijvVJCQGJAA0p
89
+ 6+3o91ccyA0zHXuUno2eMzBUDghfNZYnHg==
90
+ -----END EC PRIVATE KEY-----
91
+ EOF
92
+
93
+ PUBLIC_KEY1 = <<-EOF
94
+ -----BEGIN PUBLIC KEY-----
95
+ MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEArfTA/lKVR84IMc9ZzXOC
96
+ Hr8DVtR8hzWuEVHF6KElavRHlk14g0SZu3m908Ejm/XF3EfNHjX9wN+62IMA0QBx
97
+ kBMFCuLF+U/oeUs0NoDdAEKxjj4n6lq6Ss8aLct+anMy7D1jwvOLbcwV54w1d5JD
98
+ dlZVdZ6AvHm9otwJq6rNpDgdmXY4HgC2nM9csFpuy0cDpL6fdJx9lcNL2RnkRC4+
99
+ RMsIB+PxDw0j3vDi04dYLBXMGYjyeGH+mIFpL3PTPXGXwL2XDYXZ2H4SQX6bOoKm
100
+ azTXq6QXuEB665njh1GxXldoIMcSshoJL0hrk3WrTOG22N2CQA+IfHgrXJ+A+QUz
101
+ KQIBIw==
102
+ -----END PUBLIC KEY-----
103
+ EOF
104
+
105
+ PUBLIC_KEY2 = <<-EOF
106
+ -----BEGIN PUBLIC KEY-----
107
+ MIIBIDANBgkqhkiG9w0BAQEFAAOCAQ0AMIIBCAKCAQEAxl6TpN7uFiY/JZ8qDnD7
108
+ UrxDP+ABeh2PVg8Du1LEgXNk0+YWCeP5S6oHklqaWeDlbmAs1oHsBwCMAVpMa5tg
109
+ ONOLvz4JgwgkiqQEbKR8ofWJ+LADUElvqRVGmGiNEMLI6GJWeneL4sjmbb8d6U+M
110
+ 53c6iWG0si9XE5m7teBQSsCl0Tk3qMIkQGw5zpJeCXjZ8KpJhIJRYgexFkGgPlYR
111
+ V+UYIhxpUW90t0Ra5i6JOFYwq98k5S/6SJIZQ/A9F4JNzwLw3eVxZj0yVHWxkGz1
112
+ +TyELNY1kOyMxnZaqSfGzSQJTrnIXpdweVHuYh1LtOgedRQhCyiELeSMGwio1vRP
113
+ KwIBIw==
114
+ -----END PUBLIC KEY-----
115
+ EOF
116
+
117
+ PUBLIC_KEY3 = <<-EOF
118
+ -----BEGIN PUBLIC KEY-----
119
+ MIIBuDCCASwGByqGSM44BAEwggEfAoGBALyVy5dwVwgL3CxXzsvo8DBh58qArQLB
120
+ NIPW/f9pptmy7jD5QXzOw+12w0/z4lZ86ncoVutRMf44OABcX9ovhRl+luxB7jjp
121
+ kVXy/p2ZaqPbeyTQUtdTmXa2y4n053Jd61VeMG+iLP7+viT+Ib96y9aVUYQfCrl5
122
+ heBDUZ9cAFjdAhUAxV5zuySaRSsJHqKK+Blhh7c9A9kCgYEAqel0RUBO0MY5b3DZ
123
+ 69J/mRzUifN1O6twk4er2ph0JpryuUwZohLpcVZwqoGWmPQy/ZHmV1b3RtT9GWUa
124
+ +HUqKdMhFVOx/iq1khVfLi83whjMMvXj3ecqd0yzGxGHnSsjVKefa2ywCLHrh4nl
125
+ UVIaXI5gQpgMyVbMcromDe1WZzoDgYUAAoGBAIwTRPAEcroqOzaebiVspFcmsXxD
126
+ Q4wXQZQdho1ExW6FKS8s7/6pItmZYXTvJDwLXgq2/iK1fRRcKk2PJEaSuJR7WeNG
127
+ sJKfWmQ2UbOhqA3wWLDazIZtcMKjFzD0hM4E8qgjHjMvKDE6WgT6SFP+tqx3nnh7
128
+ pJWwsbGjSMQexpyR
129
+ -----END PUBLIC KEY-----
130
+ EOF
131
+
132
+ PUBLIC_KEY4 = <<-EOF
133
+ -----BEGIN PUBLIC KEY-----
134
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEQOAmNzXT3XN5DQdHBYCgflosVlHd
135
+ 6MUB1n9n6CCijvVJCQGJAA0p6+3o91ccyA0zHXuUno2eMzBUDghfNZYnHg==
136
+ -----END PUBLIC KEY-----
76
137
  EOF
77
138
 
78
139
  SSH_PUBLIC_KEY1 = 'AAAAB3NzaC1yc2EAAAABIwAAAQEArfTA/lKVR84IMc9ZzXOCHr8DVtR8hzWuEVHF6KElavRHlk14g0SZu3m908Ejm/XF3EfNHjX9wN+62IMA0QBxkBMFCuLF+U/oeUs0NoDdAEKxjj4n6lq6Ss8aLct+anMy7D1jwvOLbcwV54w1d5JDdlZVdZ6AvHm9otwJq6rNpDgdmXY4HgC2nM9csFpuy0cDpL6fdJx9lcNL2RnkRC4+RMsIB+PxDw0j3vDi04dYLBXMGYjyeGH+mIFpL3PTPXGXwL2XDYXZ2H4SQX6bOoKmazTXq6QXuEB665njh1GxXldoIMcSshoJL0hrk3WrTOG22N2CQA+IfHgrXJ+A+QUzKQ=='
79
140
  SSH_PUBLIC_KEY2 = 'AAAAB3NzaC1yc2EAAAABIwAAAQEAxl6TpN7uFiY/JZ8qDnD7UrxDP+ABeh2PVg8Du1LEgXNk0+YWCeP5S6oHklqaWeDlbmAs1oHsBwCMAVpMa5tgONOLvz4JgwgkiqQEbKR8ofWJ+LADUElvqRVGmGiNEMLI6GJWeneL4sjmbb8d6U+M53c6iWG0si9XE5m7teBQSsCl0Tk3qMIkQGw5zpJeCXjZ8KpJhIJRYgexFkGgPlYRV+UYIhxpUW90t0Ra5i6JOFYwq98k5S/6SJIZQ/A9F4JNzwLw3eVxZj0yVHWxkGz1+TyELNY1kOyMxnZaqSfGzSQJTrnIXpdweVHuYh1LtOgedRQhCyiELeSMGwio1vRPKw=='
80
141
  SSH_PUBLIC_KEY3 = 'AAAAB3NzaC1kc3MAAACBALyVy5dwVwgL3CxXzsvo8DBh58qArQLBNIPW/f9pptmy7jD5QXzOw+12w0/z4lZ86ncoVutRMf44OABcX9ovhRl+luxB7jjpkVXy/p2ZaqPbeyTQUtdTmXa2y4n053Jd61VeMG+iLP7+viT+Ib96y9aVUYQfCrl5heBDUZ9cAFjdAAAAFQDFXnO7JJpFKwkeoor4GWGHtz0D2QAAAIEAqel0RUBO0MY5b3DZ69J/mRzUifN1O6twk4er2ph0JpryuUwZohLpcVZwqoGWmPQy/ZHmV1b3RtT9GWUa+HUqKdMhFVOx/iq1khVfLi83whjMMvXj3ecqd0yzGxGHnSsjVKefa2ywCLHrh4nlUVIaXI5gQpgMyVbMcromDe1WZzoAAACBAIwTRPAEcroqOzaebiVspFcmsXxDQ4wXQZQdho1ExW6FKS8s7/6pItmZYXTvJDwLXgq2/iK1fRRcKk2PJEaSuJR7WeNGsJKfWmQ2UbOhqA3wWLDazIZtcMKjFzD0hM4E8qgjHjMvKDE6WgT6SFP+tqx3nnh7pJWwsbGjSMQexpyR'
142
+ SSH_PUBLIC_KEY4 = 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEDgJjc1091zeQ0HRwWAoH5aLFZR3ejFAdZ/Z+ggoo71SQkBiQANKevt6PdXHMgNMx17lJ6NnjMwVA4IXzWWJx4='
143
+
144
+ SSH_PUBLIC_KEY_ED25519 = 'AAAAC3NzaC1lZDI1NTE5AAAAIBrNsRCISAtKXV5OVxqV6unVcdis5Uh3oiC6B7CMB7HQ'
145
+ SSH_PUBLIC_KEY_ED25519_0_BYTE = 'AAAAC3NzaC1lZDI1NTE5AAAAIADK9x9t3yQQH7h4OEJpUa7l2j7mcmKf4LAsNXHxNbSm'
81
146
 
82
- SSH_PUBLIC_KEY_ED25519 = 'AAAAC3NzaC1lZDI1NTE5AAAAIBrNsRCISAtKXV5OVxqV6unVcdis5Uh3oiC6B7CMB7HQ'
83
147
  SSH_PUBLIC_KEY_ECDSA_256 = 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHJFDZ5qymZfIzoJcxYeu3C9HjJ08QAbqR28C2zSMLwcb3ZzWdRApnj6wEgRvizsBmr9zyPKb2u5Rp0vjJtQcZo='
84
148
  SSH_PUBLIC_KEY_ECDSA_384 = 'AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBP+GtUCOR8aW7xTtpkbJS0qqNZ98PgbUNtTFhE+Oe+khgoFMX+o0JG5bckVuvtkRl8dr+63kUK0QPTtzP9O5yixB9CYnB8CgCgYo1FCXZuJIImf12wW5nWKglrCH4kV1Qg=='
85
149
  SSH_PUBLIC_KEY_ECDSA_521 = 'AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACsunidnIZ77AjCHSDp/xknLGDW3M0Ia7nxLdImmp0XGbxtbwYm2ga5XUzV9dMO9wF9ICC3OuH6g9DtGOBNPru1PwFDjaPISGgm0vniEzWazLsvjJVLThOA3VyYLxmtjm0WfS+/DfxgWVS6oeCTnDjjoVVpwU/fDbUbYPPRZI84/hOGNA=='
86
150
 
151
+ SSH_PUBLIC_KEY_ECDSA_256_COMPRESSED = 'AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAAAhA+YNpJJrrUsu5OLLvqGX5pAH3+x6/yEFU2AYdxb54Jk8'
152
+ SSH_PUBLIC_KEY_ECDSA_384_COMPRESSED = 'AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAAAxAgMhp0cNvtzncxXF0W5nrkBCTrxJIcYqUTX4RcKWIM74FfxizmWJqP/C+looEz6dLQ=='
153
+ SSH_PUBLIC_KEY_ECDSA_521_COMPRESSED = 'AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAABDAgDoeNR4bndT24BosNaTKCLOALjL6tXrpNHn0HJzHO5z30L4SvH0Gz9jvAiqehNHOgmK3/bFbwLVW1W4TJbNsp8BVA=='
154
+
87
155
  KEY1_MD5_FINGERPRINT = "2a:89:84:c9:29:05:d1:f8:49:79:1c:ba:73:99:eb:af"
88
156
  KEY2_MD5_FINGERPRINT = "3c:af:74:87:cc:cc:a1:12:05:1a:09:b7:7b:ce:ed:ce"
89
157
  KEY3_MD5_FINGERPRINT = "14:f6:6a:12:96:be:44:32:e6:3c:77:43:94:52:f5:7a"
158
+ KEY4_MD5_FINGERPRINT = "38:0b:0f:63:36:64:b6:f0:43:94:de:32:75:eb:57:68"
90
159
  ED25519_MD5_FINGERPRINT = "6f:1a:8a:c1:4f:13:5c:36:6e:3f:be:eb:49:3b:8e:3e"
91
160
  ECDSA_256_MD5_FINGERPRINT = "d9:3a:7f:de:b2:65:04:ac:62:05:1a:1e:97:e9:2b:9d"
161
+ ECDSA_384_MD5_FINGERPRINT = "b5:bb:3e:f6:eb:3b:0f:1e:18:37:1f:36:ac:7c:87:0d"
162
+ ECDSA_521_MD5_FINGERPRINT = "98:8e:a9:4c:b9:aa:58:35:d1:42:65:c3:41:dd:04:e1"
92
163
 
93
164
  KEY1_SHA1_FINGERPRINT = "e4:f9:79:f2:fe:d6:be:2d:ef:2e:c2:fa:aa:f8:b0:17:34:fe:0d:c0"
94
165
  KEY2_SHA1_FINGERPRINT = "9a:52:78:2b:6b:cb:39:b7:85:ed:90:8a:28:62:aa:b3:98:88:e6:07"
95
166
  KEY3_SHA1_FINGERPRINT = "15:68:c6:72:ac:18:d1:fc:ab:a2:b7:b5:8c:d1:fe:8f:b9:ae:a9:47"
167
+ KEY4_SHA1_FINGERPRINT = "aa:b5:e6:62:27:87:b8:05:f6:d6:8f:31:dc:83:81:d9:8f:f8:71:29"
96
168
  ED25519_SHA1_FINGERPRINT = "57:41:7c:d0:e2:53:28:87:7e:87:53:d4:69:ef:ef:63:ec:c0:0e:5e"
97
169
  ECDSA_256_SHA1_FINGERPRINT = "94:e8:92:2b:1b:ec:49:de:ff:85:ea:6e:10:d6:8d:87:7a:67:40:ee"
170
+ ECDSA_384_SHA1_FINGERPRINT = "cc:fb:4c:d6:e9:d0:03:ae:2d:82:e1:fc:70:d8:47:98:25:e1:83:2b"
171
+ ECDSA_521_SHA1_FINGERPRINT = "6b:2c:a2:6e:3a:82:6c:73:28:57:91:20:71:82:bc:8f:f8:9d:6c:41"
98
172
 
99
173
  KEY1_SHA256_FINGERPRINT = "js3llFehloxCfsVuDw5xu3NtS9AOAxcXY8WL6vkDIts="
100
174
  KEY2_SHA256_FINGERPRINT = "23f/6U/LdxIFx1CQFKHylw76n+LIHYoY4nRxKcFoos4="
101
175
  KEY3_SHA256_FINGERPRINT = "mPqEPQlOPGORrTJrU17sPax1jOqeutZja6MOsFIca+8="
176
+ KEY4_SHA256_FINGERPRINT = "foUpf1ox3KfG3eKgJxGoSdZFRxHPsBYJgfD+CMYky6Y="
102
177
  ED25519_SHA256_FINGERPRINT = "gyzHUKl1eO8Bk1Cvn4joRgxRlXo1+1HJ3Vho/hAtKEg="
103
178
  ECDSA_256_SHA256_FINGERPRINT = "ncy2crhoL44R58GCZPQ5chPRrjlQKKgu07FDNelDmdk="
179
+ ECDSA_384_SHA256_FINGERPRINT = "mrr4QcP6qD05DUS6Rwefb9f0uuvjyMcO28LSiq2283U="
180
+ ECDSA_521_SHA256_FINGERPRINT = "QnaiGMIVDZyTG47hMWK6Y1z/yUzHIcTBGpNNuUwlhAk="
104
181
 
105
182
  KEY1_RANDOMART = <<-EOF.rstrip
106
183
  +--[ RSA 2048]----+
@@ -142,6 +219,81 @@ EOF
142
219
  | |
143
220
  | |
144
221
  +-----------------+
222
+ EOF
223
+
224
+ # ssh-keygen -lv -E md5 -f ./id_ecdsa_ssh_public_key4.pub
225
+ KEY4_RANDOMART = <<-EOF.rstrip
226
+ +--[ECDSA 256]----+
227
+ | .. |
228
+ | .. . . |
229
+ | ..=o . . . |
230
+ | B+.... E . |
231
+ | @oo.S. . |
232
+ | o B o. . |
233
+ | o . |
234
+ | |
235
+ | |
236
+ +-----------------+
237
+ EOF
238
+
239
+ # ssh-keygen -lv -E sha256 -f ./id_ecdsa_ssh_public_key4.pub
240
+ KEY4_RANDOMART_USING_SHA256_DIGEST = <<-EOF.rstrip
241
+ +--[ECDSA 256]----+
242
+ | .. o++B+ |
243
+ | .. ...* |
244
+ | . ...o o o |
245
+ | . =o.o .= . |
246
+ | +o+oS o.= . .|
247
+ | o .oo =.. + +.|
248
+ | E o +.+ = o|
249
+ | ..=.+ . |
250
+ | oo . |
251
+ +-----------------+
252
+ EOF
253
+
254
+ # ssh-keygen -lv -E sha384 -f ./id_ecdsa_ssh_public_key4.pub
255
+ KEY4_RANDOMART_USING_SHA384_DIGEST = <<-EOF.rstrip
256
+ +--[ECDSA 256]----+
257
+ | o++. |
258
+ | . *oo. . |
259
+ |o .o+B.o.. |
260
+ |+o ooB+O *..|
261
+ |.=+ .SB== ^.+.|
262
+ |+ o +o .O Xo.|
263
+ | . ... .. + .o|
264
+ | . E. o + + +..|
265
+ | .... . o..Bo..|
266
+ +-----------------+
267
+ EOF
268
+
269
+ # ssh-keygen -lv -E sha512 -f ./id_ecdsa_ssh_public_key4.pub
270
+ KEY4_RANDOMART_USING_SHA512_DIGEST = <<-EOF.rstrip
271
+ +--[ECDSA 256]----+
272
+ | +*+o oo|
273
+ | . .o o . +|
274
+ | . o. oo oo|
275
+ |.. .+ . .*.o+ |
276
+ |..Bo.* S ..=o..|
277
+ | .+X+ Oo ...+ |
278
+ | +o.B*+=o .+ +|
279
+ |+=+O.+=+.+. +.o+.|
280
+ |@**EB*O++=o+ =o.+|
281
+ +-----------------+
282
+ EOF
283
+
284
+ KEY1_SSHFP = <<-EOF.rstrip
285
+ localhost IN SSHFP 1 1 e4f979f2fed6be2def2ec2faaaf8b01734fe0dc0
286
+ localhost IN SSHFP 1 2 8ecde59457a1968c427ec56e0f0e71bb736d4bd00e03171763c58beaf90322db
287
+ EOF
288
+
289
+ KEY2_SSHFP = <<-EOF.rstrip
290
+ localhost IN SSHFP 1 1 9a52782b6bcb39b785ed908a2862aab39888e607
291
+ localhost IN SSHFP 1 2 db77ffe94fcb771205c7509014a1f2970efa9fe2c81d8a18e2747129c168a2ce
292
+ EOF
293
+
294
+ KEY3_SSHFP = <<-EOF.rstrip
295
+ localhost IN SSHFP 2 1 1568c672ac18d1fcaba2b7b58cd1fe8fb9aea947
296
+ localhost IN SSHFP 2 2 98fa843d094e3c6391ad326b535eec3dac758cea9ebad6636ba30eb0521c6bef
145
297
  EOF
146
298
 
147
299
  SSH2_PUBLIC_KEY1 = <<-EOF.rstrip
@@ -188,6 +340,7 @@ EOF
188
340
  @key1 = SSHKey.new(SSH_PRIVATE_KEY1, :comment => "me@example.com")
189
341
  @key2 = SSHKey.new(SSH_PRIVATE_KEY2, :comment => "me@example.com")
190
342
  @key3 = SSHKey.new(SSH_PRIVATE_KEY3, :comment => "me@example.com")
343
+ @key4 = SSHKey.new(SSH_PRIVATE_KEY4, :comment => "me@example.com")
191
344
  @key_without_comment = SSHKey.new(SSH_PRIVATE_KEY1)
192
345
  end
193
346
 
@@ -200,7 +353,14 @@ EOF
200
353
  end
201
354
 
202
355
  def test_generator_with_type
356
+ assert_equal "rsa", SSHKey.generate(:type => "rsa").type
203
357
  assert_equal "dsa", SSHKey.generate(:type => "dsa").type
358
+
359
+ if ecdsa_supported?
360
+ assert_equal "ecdsa", SSHKey.generate(:type => "ecdsa").type
361
+ else
362
+ assert_raises(NotImplementedError) { SSHKey.generate(:type => "ecdsa").type }
363
+ end
204
364
  end
205
365
 
206
366
  def test_generator_with_passphrase
@@ -222,6 +382,30 @@ EOF
222
382
  assert_equal SSH_PRIVATE_KEY3, @key3.dsa_private_key
223
383
  end
224
384
 
385
+ def test_private_key4
386
+ if ecdsa_supported?
387
+ assert_equal SSH_PRIVATE_KEY4, @key4.private_key
388
+ else
389
+ assert_raises(NotImplementedError) { @key4.private_key }
390
+ end
391
+ end
392
+
393
+ def test_public_key_1
394
+ assert_equal PUBLIC_KEY1, @key1.public_key
395
+ end
396
+
397
+ def test_public_key_2
398
+ assert_equal PUBLIC_KEY2, @key2.public_key
399
+ end
400
+
401
+ def test_public_key_3
402
+ assert_equal PUBLIC_KEY3, @key3.public_key
403
+ end
404
+
405
+ def test_public_key_4
406
+ assert_equal PUBLIC_KEY4, @key4.public_key
407
+ end
408
+
225
409
  def test_ssh_public_key_decoded1
226
410
  assert_equal Base64.decode64(SSH_PUBLIC_KEY1), @key1.send(:ssh_public_key_conversion)
227
411
  end
@@ -234,6 +418,14 @@ EOF
234
418
  assert_equal Base64.decode64(SSH_PUBLIC_KEY3), @key3.send(:ssh_public_key_conversion)
235
419
  end
236
420
 
421
+ def test_ssh_public_key_decoded4
422
+ if ecdsa_supported?
423
+ assert_equal Base64.decode64(SSH_PUBLIC_KEY4), @key4.send(:ssh_public_key_conversion)
424
+ else
425
+ assert_raises(NotImplementedError) { @key4.send(:ssh_public_key_conversion) }
426
+ end
427
+ end
428
+
237
429
  def test_ssh_public_key_encoded1
238
430
  assert_equal SSH_PUBLIC_KEY1, Base64.encode64(@key1.send(:ssh_public_key_conversion)).gsub("\n", "")
239
431
  end
@@ -246,24 +438,37 @@ EOF
246
438
  assert_equal SSH_PUBLIC_KEY3, Base64.encode64(@key3.send(:ssh_public_key_conversion)).gsub("\n", "")
247
439
  end
248
440
 
441
+ def test_ssh_public_key_encoded4
442
+ if ecdsa_supported?
443
+ assert_equal SSH_PUBLIC_KEY4, Base64.encode64(@key4.send(:ssh_public_key_conversion)).gsub("\n", "")
444
+ else
445
+ assert_raises(NotImplementedError) { Base64.encode64(@key4.send(:ssh_public_key_conversion)) }
446
+ end
447
+ end
448
+
249
449
  def test_ssh_public_key_output
250
450
  expected1 = "ssh-rsa #{SSH_PUBLIC_KEY1} me@example.com"
251
451
  expected2 = "ssh-rsa #{SSH_PUBLIC_KEY2} me@example.com"
252
452
  expected3 = "ssh-dss #{SSH_PUBLIC_KEY3} me@example.com"
253
- expected4 = "ssh-rsa #{SSH_PUBLIC_KEY1}"
453
+ expected4 = "ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY4} me@example.com"
454
+ expected1b = "ssh-rsa #{SSH_PUBLIC_KEY1}"
254
455
  assert_equal expected1, @key1.ssh_public_key
255
456
  assert_equal expected2, @key2.ssh_public_key
256
457
  assert_equal expected3, @key3.ssh_public_key
257
- assert_equal expected4, @key_without_comment.ssh_public_key
458
+
459
+ if ecdsa_supported?
460
+ assert_equal expected4, @key4.ssh_public_key
461
+ else
462
+ assert_raises(NotImplementedError) { @key4.ssh_public_key }
463
+ end
464
+
465
+ assert_equal expected1b, @key_without_comment.ssh_public_key
258
466
  end
259
467
 
260
468
  def test_ssh2_public_key_output
261
469
  expected1 = SSH2_PUBLIC_KEY1
262
470
  expected2 = SSH2_PUBLIC_KEY2
263
471
  expected3 = SSH2_PUBLIC_KEY3
264
- public_key1 = "ssh-rsa #{SSH_PUBLIC_KEY1} me@example.com"
265
- public_key2 = "ssh-rsa #{SSH_PUBLIC_KEY2}"
266
- public_key3 = "ssh-rsa #{SSH_PUBLIC_KEY3} 1024-bit DSA with provided comment"
267
472
 
268
473
  assert_equal expected1, @key1.ssh2_public_key
269
474
  assert_equal expected2, @key2.ssh2_public_key({})
@@ -271,6 +476,24 @@ EOF
271
476
  'x-private-use-header' => 'some value that is long enough to go to wrap around to a new line.'})
272
477
  end
273
478
 
479
+ def test_ssh_public_key_output_from_generated
480
+ generated_rsa = SSHKey.generate(:type => "rsa", :comment => "rsa key")
481
+ generated_dsa = SSHKey.generate(:type => "dsa", :comment => "dsa key")
482
+ generated_ecdsa = SSHKey.generate(:type => "ecdsa", :comment => "ecdsa key") if ecdsa_supported?
483
+
484
+ encoded_rsa = Base64.encode64(generated_rsa.send(:ssh_public_key_conversion)).gsub("\n", "")
485
+ encoded_dsa = Base64.encode64(generated_dsa.send(:ssh_public_key_conversion)).gsub("\n", "")
486
+ encoded_ecdsa = Base64.encode64(generated_ecdsa.send(:ssh_public_key_conversion)).gsub("\n", "") if ecdsa_supported?
487
+
488
+ expected_rsa = "ssh-rsa #{encoded_rsa} rsa key"
489
+ expected_dsa = "ssh-dss #{encoded_dsa} dsa key"
490
+ expected_ecdsa = "ecdsa-sha2-nistp256 #{encoded_ecdsa} ecdsa key"
491
+
492
+ assert_equal expected_rsa, generated_rsa.ssh_public_key
493
+ assert_equal expected_dsa, generated_dsa.ssh_public_key
494
+ assert_equal expected_ecdsa, generated_ecdsa.ssh_public_key if ecdsa_supported?
495
+ end
496
+
274
497
  def test_public_key_directives
275
498
  assert_equal [], SSHKey.generate.directives
276
499
 
@@ -324,6 +547,7 @@ EOF
324
547
 
325
548
  def test_ssh_public_key_validation_elliptic
326
549
  assert SSHKey.valid_ssh_public_key?("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519} me@example.com")
550
+ assert SSHKey.valid_ssh_public_key?("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519_0_BYTE} me@example.com")
327
551
  assert SSHKey.valid_ssh_public_key?("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256}")
328
552
  assert SSHKey.valid_ssh_public_key?("ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384} me@example.com")
329
553
  assert SSHKey.valid_ssh_public_key?(%Q{from="trusted.eng.cam.ac.uk",no-port-forwarding,no-pty ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521} me@example.com})
@@ -352,6 +576,28 @@ EOF
352
576
  assert !SSHKey.valid_ssh_public_key?(invalid4)
353
577
  end
354
578
 
579
+ def test_ssh_public_key_validation_with_comments
580
+ expected1 = "# Comment\nssh-rsa #{SSH_PUBLIC_KEY1}"
581
+ expected2 = "# First comment\n\n# Second comment\n\nssh-ed25519 #{SSH_PUBLIC_KEY_ED25519} me@example.com"
582
+ invalid1 = "No starting hash # Valid comment\nssh-rsa #{SSH_PUBLIC_KEY1} me@example.com"
583
+ invalid2 = "# First comment\n\nSecond comment without hash\n\necdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256}\nme@example.com"
584
+
585
+ assert SSHKey.valid_ssh_public_key?(expected1)
586
+ assert SSHKey.valid_ssh_public_key?(expected2)
587
+
588
+ assert !SSHKey.valid_ssh_public_key?(invalid1)
589
+ assert !SSHKey.valid_ssh_public_key?(invalid2)
590
+ end
591
+
592
+ def test_ssh_public_key_sshfp
593
+ assert_equal KEY1_SSHFP, SSHKey.sshfp("localhost", "ssh-rsa #{SSH_PUBLIC_KEY1}\n")
594
+ assert_equal KEY2_SSHFP, SSHKey.sshfp("localhost", "ssh-rsa #{SSH_PUBLIC_KEY2}\n")
595
+ assert_equal KEY3_SSHFP, SSHKey.sshfp("localhost", "ssh-dss #{SSH_PUBLIC_KEY3}\n")
596
+ assert_equal KEY1_SSHFP, SSHKey.sshfp("localhost", SSH_PRIVATE_KEY1)
597
+ assert_equal KEY2_SSHFP, SSHKey.sshfp("localhost", SSH_PRIVATE_KEY2)
598
+ assert_equal KEY3_SSHFP, SSHKey.sshfp("localhost", SSH_PRIVATE_KEY3)
599
+ end
600
+
355
601
  def test_ssh_public_key_bits
356
602
  expected1 = "ssh-rsa #{SSH_PUBLIC_KEY1} me@example.com"
357
603
  expected2 = "ssh-rsa #{SSH_PUBLIC_KEY2} me@example.com"
@@ -359,6 +605,12 @@ EOF
359
605
  expected4 = "ssh-rsa #{SSH_PUBLIC_KEY1}"
360
606
  expected5 = %Q{from="trusted.eng.cam.ac.uk",no-port-forwarding,no-pty ssh-rsa #{SSH_PUBLIC_KEY1}}
361
607
  invalid1 = "#{SSH_PUBLIC_KEY1} me@example.com"
608
+ ecdsa256 = "ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256}"
609
+ ecdsa384 = "ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384}"
610
+ ecdsa521 = "ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521}"
611
+ ecdsa256_compressed = "ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256_COMPRESSED}"
612
+ ecdsa384_compressed = "ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384_COMPRESSED}"
613
+ ecdsa521_compressed = "ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521_COMPRESSED}"
362
614
 
363
615
  assert_equal 2048, SSHKey.ssh_public_key_bits(expected1)
364
616
  assert_equal 2048, SSHKey.ssh_public_key_bits(expected2)
@@ -366,6 +618,12 @@ EOF
366
618
  assert_equal 2048, SSHKey.ssh_public_key_bits(expected4)
367
619
  assert_equal 2048, SSHKey.ssh_public_key_bits(expected5)
368
620
  assert_equal 512, SSHKey.ssh_public_key_bits(SSHKey.generate(:bits => 512).ssh_public_key)
621
+ assert_equal 256, SSHKey.ssh_public_key_bits(ecdsa256)
622
+ assert_equal 384, SSHKey.ssh_public_key_bits(ecdsa384)
623
+ assert_equal 521, SSHKey.ssh_public_key_bits(ecdsa521)
624
+ assert_equal 256, SSHKey.ssh_public_key_bits(ecdsa256_compressed)
625
+ assert_equal 384, SSHKey.ssh_public_key_bits(ecdsa384_compressed)
626
+ assert_equal 521, SSHKey.ssh_public_key_bits(ecdsa521_compressed)
369
627
 
370
628
  exception1 = assert_raises(SSHKey::PublicKeyError) { SSHKey.ssh_public_key_bits( expected1.gsub('A','.') ) }
371
629
  exception2 = assert_raises(SSHKey::PublicKeyError) { SSHKey.ssh_public_key_bits( expected1[0..-20] ) }
@@ -385,7 +643,7 @@ EOF
385
643
  assert_equal(SSH2_PUBLIC_KEY2, SSHKey.ssh_public_key_to_ssh2_public_key(public_key2))
386
644
  assert_equal(SSH2_PUBLIC_KEY2, SSHKey.ssh_public_key_to_ssh2_public_key(public_key2, {}))
387
645
  assert_equal(SSH2_PUBLIC_KEY3, SSHKey.ssh_public_key_to_ssh2_public_key(public_key3, {'Comment' => '1024-bit DSA with provided comment', 'x-private-use-header' => 'some value that is long enough to go to wrap around to a new line.'}))
388
- end
646
+ end
389
647
 
390
648
  def test_exponent
391
649
  assert_equal 35, @key1.key_object.e.to_i
@@ -403,22 +661,43 @@ end
403
661
  assert_equal KEY2_MD5_FINGERPRINT, @key2.md5_fingerprint
404
662
  assert_equal KEY3_MD5_FINGERPRINT, @key3.md5_fingerprint
405
663
 
664
+ if ecdsa_supported?
665
+ assert_equal KEY4_MD5_FINGERPRINT, @key4.md5_fingerprint
666
+ else
667
+ assert_raises(NotImplementedError) { @key4.md5_fingerprint }
668
+ end
669
+
406
670
  assert_equal KEY1_SHA1_FINGERPRINT, @key1.sha1_fingerprint
407
671
  assert_equal KEY2_SHA1_FINGERPRINT, @key2.sha1_fingerprint
408
672
  assert_equal KEY3_SHA1_FINGERPRINT, @key3.sha1_fingerprint
409
673
 
674
+ if ecdsa_supported?
675
+ assert_equal KEY4_SHA1_FINGERPRINT, @key4.sha1_fingerprint
676
+ else
677
+ assert_raises(NotImplementedError) { @key4.sha1_fingerprint }
678
+ end
679
+
410
680
  assert_equal KEY1_SHA256_FINGERPRINT, @key1.sha256_fingerprint
411
681
  assert_equal KEY2_SHA256_FINGERPRINT, @key2.sha256_fingerprint
412
682
  assert_equal KEY3_SHA256_FINGERPRINT, @key3.sha256_fingerprint
413
683
 
684
+ if ecdsa_supported?
685
+ assert_equal KEY4_SHA256_FINGERPRINT, @key4.sha256_fingerprint
686
+ else
687
+ assert_raises(NotImplementedError) { @key4.sha256_fingerprint }
688
+ end
689
+
414
690
  assert_equal KEY1_MD5_FINGERPRINT, SSHKey.md5_fingerprint(SSH_PRIVATE_KEY1)
415
691
  assert_equal KEY1_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY1}")
416
692
  assert_equal KEY2_MD5_FINGERPRINT, SSHKey.md5_fingerprint(SSH_PRIVATE_KEY2)
417
693
  assert_equal KEY2_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY2} me@me.com")
418
694
  assert_equal KEY3_MD5_FINGERPRINT, SSHKey.md5_fingerprint(SSH_PRIVATE_KEY3)
419
695
  assert_equal KEY3_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ssh-dss #{SSH_PUBLIC_KEY3}")
696
+ assert_equal KEY4_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY4}")
420
697
  assert_equal ED25519_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519}")
421
698
  assert_equal ECDSA_256_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256} me@me.com")
699
+ assert_equal ECDSA_384_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384} me@me.com")
700
+ assert_equal ECDSA_521_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521} me@me.com")
422
701
 
423
702
  assert_equal KEY1_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint(SSH_PRIVATE_KEY1)
424
703
  assert_equal KEY1_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY1}")
@@ -426,8 +705,11 @@ end
426
705
  assert_equal KEY2_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY2} me@me.com")
427
706
  assert_equal KEY3_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint(SSH_PRIVATE_KEY3)
428
707
  assert_equal KEY3_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ssh-dss #{SSH_PUBLIC_KEY3}")
708
+ assert_equal KEY4_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY4}")
429
709
  assert_equal ED25519_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519}")
430
710
  assert_equal ECDSA_256_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256} me@me.com")
711
+ assert_equal ECDSA_384_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384} me@me.com")
712
+ assert_equal ECDSA_521_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521} me@me.com")
431
713
 
432
714
  assert_equal KEY1_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint(SSH_PRIVATE_KEY1)
433
715
  assert_equal KEY1_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY1}")
@@ -435,14 +717,24 @@ end
435
717
  assert_equal KEY2_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY2} me@me.com")
436
718
  assert_equal KEY3_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint(SSH_PRIVATE_KEY3)
437
719
  assert_equal KEY3_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ssh-dss #{SSH_PUBLIC_KEY3}")
720
+ assert_equal KEY4_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY4}")
438
721
  assert_equal ED25519_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519}")
439
722
  assert_equal ECDSA_256_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY_ECDSA_256} me@me.com")
723
+ assert_equal ECDSA_384_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ecdsa-sha2-nistp384 #{SSH_PUBLIC_KEY_ECDSA_384} me@me.com")
724
+ assert_equal ECDSA_521_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ecdsa-sha2-nistp521 #{SSH_PUBLIC_KEY_ECDSA_521} me@me.com")
440
725
  end
441
726
 
442
727
  def test_bits
443
728
  assert_equal 2048, @key1.bits
444
729
  assert_equal 2048, @key2.bits
445
730
  assert_equal 1024, @key3.bits
731
+
732
+ if ecdsa_supported?
733
+ assert_equal 256, @key4.bits
734
+ else
735
+ assert_raises(NotImplementedError) { @key4.bits }
736
+ end
737
+
446
738
  assert_equal 512, SSHKey.generate(:bits => 512).bits
447
739
  end
448
740
 
@@ -450,7 +742,27 @@ end
450
742
  assert_equal KEY1_RANDOMART, @key1.randomart
451
743
  assert_equal KEY2_RANDOMART, @key2.randomart
452
744
  assert_equal KEY3_RANDOMART, @key3.randomart
745
+
746
+ if ecdsa_supported?
747
+ assert_equal KEY4_RANDOMART, @key4.randomart
748
+ else
749
+ assert_raises(NotImplementedError) { @key4.randomart }
750
+ end
751
+
752
+ if ecdsa_supported?
753
+ assert_equal KEY4_RANDOMART_USING_SHA256_DIGEST, @key4.randomart("SHA256")
754
+ assert_equal KEY4_RANDOMART_USING_SHA384_DIGEST, @key4.randomart("SHA384")
755
+ assert_equal KEY4_RANDOMART_USING_SHA512_DIGEST, @key4.randomart("SHA512")
756
+ end
757
+
453
758
  end
759
+
760
+ def test_sshfp
761
+ assert_equal KEY1_SSHFP, @key1.sshfp("localhost")
762
+ assert_equal KEY2_SSHFP, @key2.sshfp("localhost")
763
+ assert_equal KEY3_SSHFP, @key3.sshfp("localhost")
764
+ end
765
+
454
766
  end
455
767
 
456
768
  class SSHKeyEncryptedTest < Test::Unit::TestCase
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sshkey
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Miller
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-25 00:00:00.000000000 Z
11
+ date: 2023-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -45,14 +45,13 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - ".github/workflows/ci.yml"
48
49
  - ".gitignore"
49
- - ".travis.yml"
50
50
  - Gemfile
51
51
  - LICENSE
52
52
  - README.md
53
53
  - Rakefile
54
54
  - lib/sshkey.rb
55
- - lib/sshkey/exception.rb
56
55
  - lib/sshkey/version.rb
57
56
  - sshkey.gemspec
58
57
  - test/sshkey_test.rb
@@ -60,7 +59,7 @@ homepage: https://github.com/bensie/sshkey
60
59
  licenses:
61
60
  - MIT
62
61
  metadata: {}
63
- post_install_message:
62
+ post_install_message:
64
63
  rdoc_options: []
65
64
  require_paths:
66
65
  - lib
@@ -68,16 +67,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
68
67
  requirements:
69
68
  - - ">="
70
69
  - !ruby/object:Gem::Version
71
- version: '0'
70
+ version: '2.5'
72
71
  required_rubygems_version: !ruby/object:Gem::Requirement
73
72
  requirements:
74
73
  - - ">="
75
74
  - !ruby/object:Gem::Version
76
75
  version: '0'
77
76
  requirements: []
78
- rubyforge_project: sshkey
79
- rubygems_version: 2.6.8
80
- signing_key:
77
+ rubygems_version: 3.4.18
78
+ signing_key:
81
79
  specification_version: 4
82
80
  summary: SSH private/public key generator in Ruby
83
81
  test_files:
data/.travis.yml DELETED
@@ -1,20 +0,0 @@
1
- language: ruby
2
-
3
- rvm:
4
- - 1.9.3
5
- - 2.0.0
6
- - 2.1.9
7
- - 2.2.6
8
- - 2.3.3
9
- - 2.4.0
10
- - ruby-head
11
- - jruby
12
- - jruby-head
13
-
14
- matrix:
15
- allow_failures:
16
- - rvm: ruby-head
17
- - rvm: jruby-head
18
-
19
- sudo: required
20
- dist: trusty
@@ -1,3 +0,0 @@
1
- class SSHKey
2
- class PublicKeyError < StandardError; end
3
- end