sshkey 1.9.0 → 3.0.0

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