sshkey 2.0.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: b6e9c3f40bf3a5b7a9f80cadd7d98e8c4e9b41d4
4
- data.tar.gz: 362495eb1d46d0befe4c932c94318ce1e40c8ec2
2
+ SHA256:
3
+ metadata.gz: 9e933102cbc9909bfcb4564475ef8ea53427869b58c424405a588c954f3f737c
4
+ data.tar.gz: 415a21ff41613ba75ce07dab568f8560c9b3b83b0b3e7cac015f4238f20420b0
5
5
  SHA512:
6
- metadata.gz: 7f99fe44852eb551a34ff7fc8ca2dcab7e51a222e9e2bb7c9708115419e1e1e08444ca03973692547b2ce5d83d4dda593b95baa3bca19b1def9df3037bae127c
7
- data.tar.gz: 846c9b1966e56c5f405897254619367867d34855c9ebaa65f7d863ad271763427d574589c22b6570c790642a352e8fb39fb055a56ce446c18d717f17cf943f42
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/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,12 +1,10 @@
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 2.0.0+ and JRuby.
7
+ Tested / supported on CRuby 2.5+ and JRuby.
10
8
 
11
9
  ## Installation
12
10
 
@@ -16,7 +14,7 @@ Tested / supported on CRuby 2.0.0+ and JRuby.
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,7 +30,7 @@ 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
36
  f = File.read(File.expand_path("~/.ssh/id_rsa"))
@@ -43,7 +41,7 @@ k = SSHKey.new(f, comment: "foo@bar.com")
43
41
 
44
42
  #### Private and public keys
45
43
 
46
- 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.
47
45
 
48
46
  ```ruby
49
47
  k.private_key
@@ -161,7 +159,7 @@ puts k.randomart
161
159
 
162
160
  #### Original OpenSSL key object
163
161
 
164
- 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.
165
163
 
166
164
  ```ruby
167
165
  k.key_object
@@ -213,4 +211,4 @@ SSHKey.ssh_public_key_to_ssh2_public_key "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ
213
211
 
214
212
  ## Copyright
215
213
 
216
- Copyright (c) 2011-2019 James Miller
214
+ Copyright (c) 2011-2023 James Miller
@@ -1,3 +1,3 @@
1
1
  class SSHKey
2
- VERSION = "2.0.0"
2
+ VERSION = "3.0.0"
3
3
  end
data/lib/sshkey.rb CHANGED
@@ -4,6 +4,35 @@ require 'digest/md5'
4
4
  require 'digest/sha1'
5
5
  require 'digest/sha2'
6
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
35
+
7
36
  class SSHKey
8
37
  SSH_TYPES = {
9
38
  "ssh-rsa" => "rsa",
@@ -20,7 +49,23 @@ class SSHKey
20
49
  "ecdsa" => 3,
21
50
  "ed25519" => 4,
22
51
  }
23
- SSH_CONVERSION = {"rsa" => ["e", "n"], "dsa" => ["p", "q", "g", "pub_key"]}
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
+
24
69
  SSH2_LINE_LENGTH = 70 # +1 (for line wrap '/' character) must be <= 72
25
70
 
26
71
  class << self
@@ -40,17 +85,44 @@ class SSHKey
40
85
  type = options[:type] || "rsa"
41
86
 
42
87
  # JRuby modulus size must range from 512 to 1024
43
- 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
44
94
 
45
95
  bits = options[:bits] || default_bits
46
96
  cipher = OpenSSL::Cipher.new("AES-128-CBC") if options[:passphrase]
47
97
 
98
+ raise "Bits must either: #{VALID_BITS[type.downcase].join(', ')}" unless VALID_BITS[type.downcase].nil? || VALID_BITS[type.downcase].include?(bits)
99
+
48
100
  case type.downcase
49
- when "rsa" then new(OpenSSL::PKey::RSA.generate(bits).to_pem(cipher, options[:passphrase]), options)
50
- 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
+
51
120
  else
52
121
  raise "Unknown key type: #{type}"
53
122
  end
123
+
124
+ key_pem = key_object.to_pem(cipher, options[:passphrase])
125
+ new(key_pem, options)
54
126
  end
55
127
 
56
128
  # Validate an existing SSH public key
@@ -83,9 +155,27 @@ class SSHKey
83
155
  #
84
156
  # ==== Parameters
85
157
  # * ssh_public_key<~String> - "ssh-rsa AAAAB3NzaC1yc2EA...."
158
+ # * ssh_public_key<~String> - "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY...."
86
159
  #
87
160
  def ssh_public_key_bits(ssh_public_key)
88
- 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
89
179
  end
90
180
 
91
181
  # Fingerprints
@@ -194,6 +284,48 @@ class SSHKey
194
284
  return data
195
285
  end
196
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
+
197
329
  def decoded_key(key)
198
330
  Base64.decode64(parse_ssh_public_key(key).last)
199
331
  end
@@ -203,6 +335,10 @@ class SSHKey
203
335
  end
204
336
 
205
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
+
206
342
  raise PublicKeyError, "newlines are not permitted between key data" if public_key =~ /\n(?!$)/
207
343
 
208
344
  parsed = public_key.split(" ")
@@ -227,13 +363,13 @@ class SSHKey
227
363
  end
228
364
  end
229
365
 
230
- attr_reader :key_object, :type
366
+ attr_reader :key_object, :type, :typestr
231
367
  attr_accessor :passphrase, :comment
232
368
 
233
369
  # Create a new SSHKey object
234
370
  #
235
371
  # ==== Parameters
236
- # * private_key - Existing RSA or DSA private key
372
+ # * private_key - Existing RSA or DSA or ECDSA private key
237
373
  # * options<~Hash>
238
374
  # * :comment<~String> - Comment to use for the public key, defaults to ""
239
375
  # * :passphrase<~String> - If the key is encrypted, supply the passphrase
@@ -243,19 +379,41 @@ class SSHKey
243
379
  @passphrase = options[:passphrase]
244
380
  @comment = options[:comment] || ""
245
381
  self.directives = options[:directives] || []
382
+
246
383
  begin
247
384
  @key_object = OpenSSL::PKey::RSA.new(private_key, passphrase)
248
385
  @type = "rsa"
249
- rescue
386
+ @typestr = "ssh-rsa"
387
+ rescue OpenSSL::PKey::RSAError
388
+ @type = nil
389
+ end
390
+
391
+ return if @type
392
+
393
+ begin
250
394
  @key_object = OpenSSL::PKey::DSA.new(private_key, passphrase)
251
395
  @type = "dsa"
396
+ @typestr = "ssh-dss"
397
+ rescue OpenSSL::PKey::DSAError
398
+ @type = nil
252
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}"
253
407
  end
254
408
 
255
- # Fetch the RSA/DSA private key
409
+ # Fetch the private key (PEM format)
256
410
  #
257
411
  # rsa_private_key and dsa_private_key are aliased for backward compatibility
258
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
+
259
417
  key_object.to_pem
260
418
  end
261
419
  alias_method :rsa_private_key, :private_key
@@ -269,18 +427,73 @@ class SSHKey
269
427
  key_object.to_pem(OpenSSL::Cipher.new("AES-128-CBC"), passphrase)
270
428
  end
271
429
 
272
- # Fetch the RSA/DSA public key
430
+ # Fetch the public key (PEM format)
273
431
  #
274
432
  # rsa_public_key and dsa_public_key are aliased for backward compatibility
275
433
  def public_key
276
- key_object.public_key.to_pem
434
+ public_key_object.to_pem
277
435
  end
278
436
  alias_method :rsa_public_key, :public_key
279
437
  alias_method :dsa_public_key, :public_key
280
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
+
281
494
  # SSH public key
282
495
  def ssh_public_key
283
- [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
284
497
  end
285
498
 
286
499
  # SSH2 public key (RFC4716)
@@ -323,6 +536,7 @@ class SSHKey
323
536
  #
324
537
  # Generate OpenSSH compatible ASCII art fingerprints
325
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)
326
540
  #
327
541
  # Example:
328
542
  # +--[ RSA 2048]----+
@@ -336,13 +550,23 @@ class SSHKey
336
550
  # | . . |
337
551
  # | Eo. |
338
552
  # +-----------------+
339
- def randomart
553
+ def randomart(dgst_alg = "MD5")
340
554
  fieldsize_x = 17
341
555
  fieldsize_y = 9
342
556
  x = fieldsize_x / 2
343
557
  y = fieldsize_y / 2
344
- raw_digest = Digest::MD5.digest(ssh_public_key_conversion)
345
- 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
346
570
 
347
571
  field = Array.new(fieldsize_x) { Array.new(fieldsize_y) {0} }
348
572
 
@@ -354,20 +578,27 @@ class SSHKey
354
578
  x = [[x, 0].max, fieldsize_x - 1].min
355
579
  y = [[y, 0].max, fieldsize_y - 1].min
356
580
 
357
- field[x][y] += 1 if (field[x][y] < num_bytes - 2)
581
+ field[x][y] += 1 if (field[x][y] < len - 2)
358
582
 
359
583
  byte >>= 2
360
584
  end
361
585
  end
362
586
 
363
- field[fieldsize_x / 2][fieldsize_y / 2] = num_bytes - 1
364
- field[x][y] = num_bytes
365
- augmentation_string = " .o+=*BOX@%&#/^SE"
366
- 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
+
367
598
  fieldsize_y.times do |y|
368
599
  output << "|"
369
600
  fieldsize_x.times do |x|
370
- output << augmentation_string[[field[x][y], num_bytes].min]
601
+ output << augmentation_string[[field[x][y], len].min]
371
602
  end
372
603
  output << "|"
373
604
  output << "\n"
@@ -387,6 +618,26 @@ class SSHKey
387
618
 
388
619
  private
389
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
+
390
641
  # SSH Public Key Conversion
391
642
  #
392
643
  # All data type encoding is defined in the section #5 of RFC #4251.
@@ -397,26 +648,20 @@ class SSHKey
397
648
  # For instance, the "ssh-rsa" string is encoded as the following byte array
398
649
  # [0, 0, 0, 7, 's', 's', 'h', '-', 'r', 's', 'a']
399
650
  def ssh_public_key_conversion
400
- typestr = SSH_TYPES.invert[type]
401
651
  methods = SSH_CONVERSION[type]
402
- pubkey = key_object.public_key
403
- methods.inject([7].pack("N") + typestr) do |pubkeystr, m|
404
- # Given pubkey.class == OpenSSL::BN, pubkey.to_s(0) returns an MPI
405
- # formatted string (length prefixed bytes). This is not supported by
406
- # JRuby, so we still have to deal with length and data separately.
407
- val = pubkey.send(m)
408
-
409
- # Get byte-representation of absolute value of val
410
- data = val.to_s(2)
411
-
412
- first_byte = data[0,1].unpack("c").first
413
- if val < 0
414
- # For negative values, highest bit must be set
415
- data[0] = [0x80 & first_byte].pack("c")
416
- elsif first_byte < 0
417
- # For positive values where highest bit would be set, prefix with \0
418
- 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}"
419
663
  end
664
+
420
665
  pubkeystr + [data.length].pack("N") + data
421
666
  end
422
667
  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,11 +80,66 @@ 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='
81
143
 
82
144
  SSH_PUBLIC_KEY_ED25519 = 'AAAAC3NzaC1lZDI1NTE5AAAAIBrNsRCISAtKXV5OVxqV6unVcdis5Uh3oiC6B7CMB7HQ'
83
145
  SSH_PUBLIC_KEY_ED25519_0_BYTE = 'AAAAC3NzaC1lZDI1NTE5AAAAIADK9x9t3yQQH7h4OEJpUa7l2j7mcmKf4LAsNXHxNbSm'
@@ -86,23 +148,36 @@ EOF
86
148
  SSH_PUBLIC_KEY_ECDSA_384 = 'AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBP+GtUCOR8aW7xTtpkbJS0qqNZ98PgbUNtTFhE+Oe+khgoFMX+o0JG5bckVuvtkRl8dr+63kUK0QPTtzP9O5yixB9CYnB8CgCgYo1FCXZuJIImf12wW5nWKglrCH4kV1Qg=='
87
149
  SSH_PUBLIC_KEY_ECDSA_521 = 'AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBACsunidnIZ77AjCHSDp/xknLGDW3M0Ia7nxLdImmp0XGbxtbwYm2ga5XUzV9dMO9wF9ICC3OuH6g9DtGOBNPru1PwFDjaPISGgm0vniEzWazLsvjJVLThOA3VyYLxmtjm0WfS+/DfxgWVS6oeCTnDjjoVVpwU/fDbUbYPPRZI84/hOGNA=='
88
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
+
89
155
  KEY1_MD5_FINGERPRINT = "2a:89:84:c9:29:05:d1:f8:49:79:1c:ba:73:99:eb:af"
90
156
  KEY2_MD5_FINGERPRINT = "3c:af:74:87:cc:cc:a1:12:05:1a:09:b7:7b:ce:ed:ce"
91
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"
92
159
  ED25519_MD5_FINGERPRINT = "6f:1a:8a:c1:4f:13:5c:36:6e:3f:be:eb:49:3b:8e:3e"
93
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"
94
163
 
95
164
  KEY1_SHA1_FINGERPRINT = "e4:f9:79:f2:fe:d6:be:2d:ef:2e:c2:fa:aa:f8:b0:17:34:fe:0d:c0"
96
165
  KEY2_SHA1_FINGERPRINT = "9a:52:78:2b:6b:cb:39:b7:85:ed:90:8a:28:62:aa:b3:98:88:e6:07"
97
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"
98
168
  ED25519_SHA1_FINGERPRINT = "57:41:7c:d0:e2:53:28:87:7e:87:53:d4:69:ef:ef:63:ec:c0:0e:5e"
99
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"
100
172
 
101
173
  KEY1_SHA256_FINGERPRINT = "js3llFehloxCfsVuDw5xu3NtS9AOAxcXY8WL6vkDIts="
102
174
  KEY2_SHA256_FINGERPRINT = "23f/6U/LdxIFx1CQFKHylw76n+LIHYoY4nRxKcFoos4="
103
175
  KEY3_SHA256_FINGERPRINT = "mPqEPQlOPGORrTJrU17sPax1jOqeutZja6MOsFIca+8="
176
+ KEY4_SHA256_FINGERPRINT = "foUpf1ox3KfG3eKgJxGoSdZFRxHPsBYJgfD+CMYky6Y="
104
177
  ED25519_SHA256_FINGERPRINT = "gyzHUKl1eO8Bk1Cvn4joRgxRlXo1+1HJ3Vho/hAtKEg="
105
178
  ECDSA_256_SHA256_FINGERPRINT = "ncy2crhoL44R58GCZPQ5chPRrjlQKKgu07FDNelDmdk="
179
+ ECDSA_384_SHA256_FINGERPRINT = "mrr4QcP6qD05DUS6Rwefb9f0uuvjyMcO28LSiq2283U="
180
+ ECDSA_521_SHA256_FINGERPRINT = "QnaiGMIVDZyTG47hMWK6Y1z/yUzHIcTBGpNNuUwlhAk="
106
181
 
107
182
  KEY1_RANDOMART = <<-EOF.rstrip
108
183
  +--[ RSA 2048]----+
@@ -144,6 +219,66 @@ EOF
144
219
  | |
145
220
  | |
146
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
+ +-----------------+
147
282
  EOF
148
283
 
149
284
  KEY1_SSHFP = <<-EOF.rstrip
@@ -205,6 +340,7 @@ EOF
205
340
  @key1 = SSHKey.new(SSH_PRIVATE_KEY1, :comment => "me@example.com")
206
341
  @key2 = SSHKey.new(SSH_PRIVATE_KEY2, :comment => "me@example.com")
207
342
  @key3 = SSHKey.new(SSH_PRIVATE_KEY3, :comment => "me@example.com")
343
+ @key4 = SSHKey.new(SSH_PRIVATE_KEY4, :comment => "me@example.com")
208
344
  @key_without_comment = SSHKey.new(SSH_PRIVATE_KEY1)
209
345
  end
210
346
 
@@ -217,7 +353,14 @@ EOF
217
353
  end
218
354
 
219
355
  def test_generator_with_type
356
+ assert_equal "rsa", SSHKey.generate(:type => "rsa").type
220
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
221
364
  end
222
365
 
223
366
  def test_generator_with_passphrase
@@ -239,6 +382,30 @@ EOF
239
382
  assert_equal SSH_PRIVATE_KEY3, @key3.dsa_private_key
240
383
  end
241
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
+
242
409
  def test_ssh_public_key_decoded1
243
410
  assert_equal Base64.decode64(SSH_PUBLIC_KEY1), @key1.send(:ssh_public_key_conversion)
244
411
  end
@@ -251,6 +418,14 @@ EOF
251
418
  assert_equal Base64.decode64(SSH_PUBLIC_KEY3), @key3.send(:ssh_public_key_conversion)
252
419
  end
253
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
+
254
429
  def test_ssh_public_key_encoded1
255
430
  assert_equal SSH_PUBLIC_KEY1, Base64.encode64(@key1.send(:ssh_public_key_conversion)).gsub("\n", "")
256
431
  end
@@ -263,15 +438,31 @@ EOF
263
438
  assert_equal SSH_PUBLIC_KEY3, Base64.encode64(@key3.send(:ssh_public_key_conversion)).gsub("\n", "")
264
439
  end
265
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
+
266
449
  def test_ssh_public_key_output
267
450
  expected1 = "ssh-rsa #{SSH_PUBLIC_KEY1} me@example.com"
268
451
  expected2 = "ssh-rsa #{SSH_PUBLIC_KEY2} me@example.com"
269
452
  expected3 = "ssh-dss #{SSH_PUBLIC_KEY3} me@example.com"
270
- expected4 = "ssh-rsa #{SSH_PUBLIC_KEY1}"
453
+ expected4 = "ecdsa-sha2-nistp256 #{SSH_PUBLIC_KEY4} me@example.com"
454
+ expected1b = "ssh-rsa #{SSH_PUBLIC_KEY1}"
271
455
  assert_equal expected1, @key1.ssh_public_key
272
456
  assert_equal expected2, @key2.ssh_public_key
273
457
  assert_equal expected3, @key3.ssh_public_key
274
- 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
275
466
  end
276
467
 
277
468
  def test_ssh2_public_key_output
@@ -285,6 +476,24 @@ EOF
285
476
  'x-private-use-header' => 'some value that is long enough to go to wrap around to a new line.'})
286
477
  end
287
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
+
288
497
  def test_public_key_directives
289
498
  assert_equal [], SSHKey.generate.directives
290
499
 
@@ -367,6 +576,19 @@ EOF
367
576
  assert !SSHKey.valid_ssh_public_key?(invalid4)
368
577
  end
369
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
+
370
592
  def test_ssh_public_key_sshfp
371
593
  assert_equal KEY1_SSHFP, SSHKey.sshfp("localhost", "ssh-rsa #{SSH_PUBLIC_KEY1}\n")
372
594
  assert_equal KEY2_SSHFP, SSHKey.sshfp("localhost", "ssh-rsa #{SSH_PUBLIC_KEY2}\n")
@@ -383,6 +605,12 @@ EOF
383
605
  expected4 = "ssh-rsa #{SSH_PUBLIC_KEY1}"
384
606
  expected5 = %Q{from="trusted.eng.cam.ac.uk",no-port-forwarding,no-pty ssh-rsa #{SSH_PUBLIC_KEY1}}
385
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}"
386
614
 
387
615
  assert_equal 2048, SSHKey.ssh_public_key_bits(expected1)
388
616
  assert_equal 2048, SSHKey.ssh_public_key_bits(expected2)
@@ -390,6 +618,12 @@ EOF
390
618
  assert_equal 2048, SSHKey.ssh_public_key_bits(expected4)
391
619
  assert_equal 2048, SSHKey.ssh_public_key_bits(expected5)
392
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)
393
627
 
394
628
  exception1 = assert_raises(SSHKey::PublicKeyError) { SSHKey.ssh_public_key_bits( expected1.gsub('A','.') ) }
395
629
  exception2 = assert_raises(SSHKey::PublicKeyError) { SSHKey.ssh_public_key_bits( expected1[0..-20] ) }
@@ -427,22 +661,43 @@ EOF
427
661
  assert_equal KEY2_MD5_FINGERPRINT, @key2.md5_fingerprint
428
662
  assert_equal KEY3_MD5_FINGERPRINT, @key3.md5_fingerprint
429
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
+
430
670
  assert_equal KEY1_SHA1_FINGERPRINT, @key1.sha1_fingerprint
431
671
  assert_equal KEY2_SHA1_FINGERPRINT, @key2.sha1_fingerprint
432
672
  assert_equal KEY3_SHA1_FINGERPRINT, @key3.sha1_fingerprint
433
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
+
434
680
  assert_equal KEY1_SHA256_FINGERPRINT, @key1.sha256_fingerprint
435
681
  assert_equal KEY2_SHA256_FINGERPRINT, @key2.sha256_fingerprint
436
682
  assert_equal KEY3_SHA256_FINGERPRINT, @key3.sha256_fingerprint
437
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
+
438
690
  assert_equal KEY1_MD5_FINGERPRINT, SSHKey.md5_fingerprint(SSH_PRIVATE_KEY1)
439
691
  assert_equal KEY1_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY1}")
440
692
  assert_equal KEY2_MD5_FINGERPRINT, SSHKey.md5_fingerprint(SSH_PRIVATE_KEY2)
441
693
  assert_equal KEY2_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY2} me@me.com")
442
694
  assert_equal KEY3_MD5_FINGERPRINT, SSHKey.md5_fingerprint(SSH_PRIVATE_KEY3)
443
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}")
444
697
  assert_equal ED25519_MD5_FINGERPRINT, SSHKey.md5_fingerprint("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519}")
445
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")
446
701
 
447
702
  assert_equal KEY1_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint(SSH_PRIVATE_KEY1)
448
703
  assert_equal KEY1_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY1}")
@@ -450,8 +705,11 @@ EOF
450
705
  assert_equal KEY2_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY2} me@me.com")
451
706
  assert_equal KEY3_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint(SSH_PRIVATE_KEY3)
452
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}")
453
709
  assert_equal ED25519_SHA1_FINGERPRINT, SSHKey.sha1_fingerprint("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519}")
454
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")
455
713
 
456
714
  assert_equal KEY1_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint(SSH_PRIVATE_KEY1)
457
715
  assert_equal KEY1_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY1}")
@@ -459,14 +717,24 @@ EOF
459
717
  assert_equal KEY2_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ssh-rsa #{SSH_PUBLIC_KEY2} me@me.com")
460
718
  assert_equal KEY3_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint(SSH_PRIVATE_KEY3)
461
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}")
462
721
  assert_equal ED25519_SHA256_FINGERPRINT, SSHKey.sha256_fingerprint("ssh-ed25519 #{SSH_PUBLIC_KEY_ED25519}")
463
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")
464
725
  end
465
726
 
466
727
  def test_bits
467
728
  assert_equal 2048, @key1.bits
468
729
  assert_equal 2048, @key2.bits
469
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
+
470
738
  assert_equal 512, SSHKey.generate(:bits => 512).bits
471
739
  end
472
740
 
@@ -474,6 +742,19 @@ EOF
474
742
  assert_equal KEY1_RANDOMART, @key1.randomart
475
743
  assert_equal KEY2_RANDOMART, @key2.randomart
476
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
+
477
758
  end
478
759
 
479
760
  def test_sshfp
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: 2.0.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: 2019-02-11 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,8 +45,8 @@ 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
@@ -59,7 +59,7 @@ homepage: https://github.com/bensie/sshkey
59
59
  licenses:
60
60
  - MIT
61
61
  metadata: {}
62
- post_install_message:
62
+ post_install_message:
63
63
  rdoc_options: []
64
64
  require_paths:
65
65
  - lib
@@ -67,16 +67,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
67
67
  requirements:
68
68
  - - ">="
69
69
  - !ruby/object:Gem::Version
70
- version: '0'
70
+ version: '2.5'
71
71
  required_rubygems_version: !ruby/object:Gem::Requirement
72
72
  requirements:
73
73
  - - ">="
74
74
  - !ruby/object:Gem::Version
75
75
  version: '0'
76
76
  requirements: []
77
- rubyforge_project: sshkey
78
- rubygems_version: 2.5.2.3
79
- signing_key:
77
+ rubygems_version: 3.4.18
78
+ signing_key:
80
79
  specification_version: 4
81
80
  summary: SSH private/public key generator in Ruby
82
81
  test_files:
data/.travis.yml DELETED
@@ -1,21 +0,0 @@
1
- language: ruby
2
-
3
- rvm:
4
- - 2.0
5
- - 2.1
6
- - 2.2
7
- - 2.3
8
- - 2.4
9
- - 2.5
10
- - 2.6
11
- - ruby-head
12
- - jruby
13
- - jruby-head
14
-
15
- matrix:
16
- allow_failures:
17
- - rvm: ruby-head
18
- - rvm: jruby-head
19
-
20
- sudo: required
21
- dist: xenial