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