openssl-additions 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/lib/openssl/pkey.rb +174 -20
  3. metadata +3 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cee035df22bf1142e50e33aae37f15ccebdaa7324641295983a36c5945c25766
4
- data.tar.gz: 2728021c6285d895d92dcbc66f0b25ae4367f4ea932471e717cd16318af99aa9
3
+ metadata.gz: e579a780c0c156cf7e9921d632043963b784ba0fdf5c01ebd877e6ab4f49d4f4
4
+ data.tar.gz: b984f65aabc9d6715cb1e9a86b4f8cb18f670990b360bf1f80d7a2ec6b767a71
5
5
  SHA512:
6
- metadata.gz: d68d3fe7843d3fde287d2e8337ab3e737b94389893418765c2a0b7c7b24a394b2c0c7ee58c1ff6b3ee788b63b91cadaa9ba1f97fe804a6bc4c3828f809ffa3ac
7
- data.tar.gz: 21d4b75aedb2bb6eb5ca024857c3c75a1ab4b38c8ace6deb867f826e0798e2ffd4e905cb6632f150b7367b8aec4f7b59ce738672e9e2b38561be0754b4a1915a
6
+ metadata.gz: 8beaadc77e7a316a32d019c06653122f25c309eced92f2908e2c58a738bdc77d5b91d66659e2ff7e421bf50503cd7027afe35e10c0b48d9d002d5c20bc5c5d16
7
+ data.tar.gz: d0b27c304bd94da7c91a124c2dce1815f11354e30753f895872db19a12f73d02c2c572f63b0e250752521db482b803ea545dee4252395b612834c260fbf1cff9
@@ -3,31 +3,182 @@ require "openssl"
3
3
  # Enhancements to the core asymmetric key handling.
4
4
  module OpenSSL::PKey
5
5
  # A mapping of the "SSH" names for various curves, to their OpenSSL
6
- # equivalent names.
6
+ # equivalents.
7
7
  SSH_CURVE_NAME_MAP = {
8
8
  "nistp256" => "prime256v1",
9
9
  "nistp384" => "secp384r1",
10
10
  "nistp521" => "secp521r1",
11
11
  }
12
12
 
13
- # Create a new `OpenSSL::PKey` from an SSH public key.
13
+ # Create a new `OpenSSL::PKey` from an SSH public or private key.
14
14
  #
15
- # Given an OpenSSL 2 public key (with or without the `ssh-rsa` / `ecdsa-etc`
16
- # prefix), create an equivalent instance of an `OpenSSL::PKey::PKey` subclass
17
- # which represents the same key parameters.
15
+ # Given an OpenSSH 2 public key (with or without the `ssh-rsa` / `ecdsa-etc`
16
+ # prefix), or an encrypted or unencrypted OpenSSH private key, create an
17
+ # equivalent instance of an `OpenSSL::PKey::PKey` subclass which represents
18
+ # the same key parameters.
18
19
  #
19
- # If you've got an SSH *private* key, you don't need this method, as they're
20
- # already PKCS#8 ("PEM") private keys, which OpenSSL is happy to read
21
- # directly (using `OpenSSL::PKey.read`).
20
+ # @param s [String] the SSH public or private key to convert. Public keys
21
+ # should be in their usual all-on-one-line bas64-encoded form, with or
22
+ # without the key type prefix. Private keys must have the `-----BEGIN/END
23
+ # OPENSSH PRIVATE KEY-----` delimiters.
22
24
  #
23
- # @param s [String] the SSH public key to convert, in its usual
24
- # base64-encoded form, with or without key type prefix.
25
+ # @param passphrase [String] if an encrypted private key is provided, this
26
+ # passphrase will be used to try and decrypt the key. If the passphrase
27
+ # is incorrect, an exception will be raised.
25
28
  #
26
- # @return [OpenSSL::PKey::PKey] the OpenSSL-compatible key object. Note
27
- # that this can only ever be a *public* key, never a private key, because
28
- # SSH public keys are, well, public.
29
+ # @yield if the key data passed is an encrypted private key and no passphrase
30
+ # was given, the block (if provided) will be called, and whatever the value
31
+ # of that block call is, it will be used to try and decrypt the private
32
+ # key.
29
33
  #
30
- def self.from_ssh_key(s)
34
+ # @return [OpenSSL::PKey::PKey] the OpenSSL-compatible key object.
35
+ #
36
+ # @raise [OpenSSL::PKey::PKeyError] if anything went wrong with the decoding
37
+ # process.
38
+ #
39
+ def self.from_ssh_key(s, &blk)
40
+ if s =~ /\A-----BEGIN OPENSSH PRIVATE KEY-----/
41
+ decode_private_ssh_key(s, &blk)
42
+ else
43
+ decode_public_ssh_key(s)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def self.decode_private_ssh_key(s, &blk)
50
+ unless s =~ /-----BEGIN OPENSSH PRIVATE KEY-----\n([A-Za-z0-9\/+\n]*={0,2})\n-----END OPENSSH PRIVATE KEY-----/m
51
+ raise OpenSSL::PKey::PKeyError,
52
+ "invalid OpenSSH private key format"
53
+ end
54
+
55
+ keyblob = unpack_private_ssh_key($1, &blk)
56
+ keytype, rest = ssh_key_lv_decode(keyblob, 1)
57
+
58
+ case keytype
59
+ when "ssh-rsa"
60
+ parts = ssh_key_lv_decode(rest, 6)
61
+ OpenSSL::PKey::RSA.new.tap do |k|
62
+ k.n = ssh_key_mpi_decode(parts[0])
63
+ k.e = ssh_key_mpi_decode(parts[1])
64
+ k.d = ssh_key_mpi_decode(parts[2])
65
+ k.iqmp = ssh_key_mpi_decode(parts[3])
66
+ k.p = ssh_key_mpi_decode(parts[4])
67
+ k.q = ssh_key_mpi_decode(parts[5])
68
+ end
69
+ when "ssh-dss"
70
+ parts = ssh_key_lv_decode(rest, 5)
71
+ OpenSSL::PKey::DSA.new.tap do |k|
72
+ k.p = ssh_key_mpi_decode(parts[0])
73
+ k.q = ssh_key_mpi_decode(parts[1])
74
+ k.g = ssh_key_mpi_decode(parts[2])
75
+ k.pub_key = ssh_key_mpi_decode(parts[3])
76
+ k.priv_key = ssh_key_mpi_decode(parts[4])
77
+ end
78
+ when /ecdsa-sha2-/
79
+ parts = ssh_key_lv_decode(rest, 3)
80
+
81
+ begin
82
+ OpenSSL::PKey::EC.new(SSH_CURVE_NAME_MAP[parts[0]]).tap do |k|
83
+ k.public_key = OpenSSL::PKey::EC::Point.new(k.group, parts[1])
84
+ k.private_key = ssh_key_mpi_decode(parts[2])
85
+ end
86
+ rescue TypeError
87
+ raise OpenSSL::PKey::PKeyError.new,
88
+ "Unknown curve identifier #{parts[0]}"
89
+ end
90
+ else
91
+ raise OpenSSL::PKey::PKeyError,
92
+ "Unknown key type #{keytype}"
93
+ end
94
+ end
95
+
96
+ def self.unpack_private_ssh_key(s)
97
+ rest = s.unpack("m").first
98
+
99
+ # From https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.key?annotate=HEAD
100
+
101
+ # 8: #define AUTH_MAGIC "openssh-key-v1"
102
+ # 9:
103
+ # 10: byte[] AUTH_MAGIC
104
+ unless rest[0, 15] == "openssh-key-v1\0"
105
+ raise OpenSSL::PKey::PKeyError,
106
+ "Invalid OpenSSH private key: incorrect magic found (#{rest[0, 15].inspect})"
107
+ end
108
+
109
+ # 11: string ciphername
110
+ # 12: string kdfname
111
+ # 13: string kdfoptions
112
+ cipher, kdf, kdfopts, rest = ssh_key_lv_decode(rest[15..], 3)
113
+
114
+ # 14: int number of keys N
115
+ key_count, rest = rest.unpack("Na*")
116
+ if key_count == 0
117
+ raise OpenSSL::PKey::PKeyError,
118
+ "invalid OpenSSH private key: no keys!"
119
+ elsif key_count > 1
120
+ raise OpenSSL::PKey::PKeyError,
121
+ "unsupported OpenSSH private key: multiple keys"
122
+ end
123
+
124
+ # 15: string publickey1
125
+ # We care not for your stinky public key
126
+ _, rest = ssh_key_lv_decode(rest, 1)
127
+
128
+ # 19: string encrypted, padded list of private keys
129
+ rest, x = ssh_key_lv_decode(rest, 1)
130
+ unless x.nil?
131
+ #:nocov:
132
+ raise OpenSSL::PKey::PKeyError,
133
+ "invalid OpenSSH private key: trailing garbage after private key blob: #{x.inspect}"
134
+ #:nocov:
135
+ end
136
+
137
+ if kdf == "none"
138
+ # This one is easy
139
+ # 36: uint32 checkint
140
+ # 37: uint32 checkint
141
+ # 38: string privatekey1
142
+ check1, check2, rest = rest.unpack("NNa*")
143
+ unless check1 == check2
144
+ raise OpenSSL::PKey::PKeyError,
145
+ "invalid OpenSSH private key: check values don't match"
146
+ end
147
+
148
+ # The format spec says that the keyblob is, itself, a string, but that's
149
+ # not what I'm seeing in real-world keys generated by OpenSSH -- the
150
+ # first element of the keyblob (the key type string) is just straight up
151
+ # there after the check digits. That must make parsing out multiple
152
+ # private keys an absolute nightmare -- except, oh wait (from
153
+ # openssh-portable.git/sshkey.c):
154
+ #
155
+ # if (nkeys != 1) {
156
+ # /* XXX only one key supported */
157
+ # r = SSH_ERR_INVALID_FORMAT;
158
+ # goto out;
159
+ # }
160
+ #
161
+ # Cheaters.
162
+ #
163
+ # At any rate, the fact that the spec isn't what's implemented means that
164
+ # the keyblob I do send back needs to be carefully parsed itself, rather
165
+ # than just being able to blat it through ssh_key_lv_decode to get all
166
+ # the bits. Sigh.
167
+ return rest
168
+ elsif kdf == "bcrypt"
169
+ # This is cheating a little bit, but I'm not up for implementing
170
+ # decryption support today, and this at least allows us to reliably
171
+ # detect that the key *is* in fact encrypted rather than corrupted
172
+ yield if block_given?
173
+ raise OpenSSL::PKey::PKeyError,
174
+ "unsupported OpenSSH private key: decryption is not (yet) supported"
175
+ else
176
+ raise OpenSSL::PKey::PKeyError,
177
+ "unsupport OpenSSH private key KDF #{kdf.inspect}"
178
+ end
179
+ end
180
+
181
+ def self.decode_public_ssh_key(s)
31
182
  if s =~ /\Assh-[a-z0-9-]+ /
32
183
  # WHOOP WHOOP prefixed key detected.
33
184
  s = s.split(" ")[1]
@@ -41,7 +192,7 @@ module OpenSSL::PKey
41
192
  "Invalid key encoding (not valid base64)"
42
193
  end
43
194
 
44
- parts = ssh_key_lv_decode(s)
195
+ parts = ssh_key_lv_decode(s.unpack("m").first)
45
196
 
46
197
  case parts.first
47
198
  when "ssh-rsa"
@@ -70,15 +221,13 @@ module OpenSSL::PKey
70
221
  end
71
222
  end
72
223
 
73
- private
74
-
75
224
  # Take the base64 string and split it into its component parts.
76
225
  #
77
- def self.ssh_key_lv_decode(s)
78
- rest = s.unpack("m").first
226
+ def self.ssh_key_lv_decode(s, n = nil)
227
+ rest = s
79
228
 
80
229
  [].tap do |parts|
81
- until rest == ""
230
+ until rest == "" || (n && n <= 0)
82
231
  len, rest = rest.unpack("Na*")
83
232
  if len > rest.length
84
233
  raise OpenSSL::PKey::PKeyError,
@@ -87,6 +236,11 @@ module OpenSSL::PKey
87
236
 
88
237
  elem, rest = rest.unpack("a#{len}a*")
89
238
  parts << elem
239
+ n -= 1 if n
240
+ end
241
+
242
+ if rest != ""
243
+ parts << rest
90
244
  end
91
245
  end
92
246
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openssl-additions
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Palmer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-12-20 00:00:00.000000000 Z
11
+ date: 2019-05-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -189,8 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
189
  - !ruby/object:Gem::Version
190
190
  version: '0'
191
191
  requirements: []
192
- rubyforge_project:
193
- rubygems_version: 2.7.6
192
+ rubygems_version: 3.0.1
194
193
  signing_key:
195
194
  specification_version: 4
196
195
  summary: Quality-of-life improvements to the core openssl ruby library