sshkey 2.0.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: 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