openssl-additions 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e579a780c0c156cf7e9921d632043963b784ba0fdf5c01ebd877e6ab4f49d4f4
4
- data.tar.gz: b984f65aabc9d6715cb1e9a86b4f8cb18f670990b360bf1f80d7a2ec6b767a71
3
+ metadata.gz: c2919b9f7c9b0a1a2166bcb5bd1e7273f7ac23b9efc05473c2a6ef4647b4b24d
4
+ data.tar.gz: cf8c7640fde294f77a01643a68d4befb4511db17d6f6fa38c37100de04d59c25
5
5
  SHA512:
6
- metadata.gz: 8beaadc77e7a316a32d019c06653122f25c309eced92f2908e2c58a738bdc77d5b91d66659e2ff7e421bf50503cd7027afe35e10c0b48d9d002d5c20bc5c5d16
7
- data.tar.gz: d0b27c304bd94da7c91a124c2dce1815f11354e30753f895872db19a12f73d02c2c572f63b0e250752521db482b803ea545dee4252395b612834c260fbf1cff9
6
+ metadata.gz: 92dbce64ce5e27996a9cbe3208fba4a62e3a36da5a3cf4188942bfe7e77c4e9509f0692b226962341d7e15a7532e48072481115be3b9e502b36c41a7166eba6c
7
+ data.tar.gz: add7918b67e7e5e369a89a0231bb32d775434244d11e0daac325330e4adbdeca9606efec686f308c79a1d38b236775766ca7299a406a8b69a0847acd7a00c4f2
@@ -1,254 +1 @@
1
- require "openssl"
2
-
3
- # Enhancements to the core asymmetric key handling.
4
- module OpenSSL::PKey
5
- # A mapping of the "SSH" names for various curves, to their OpenSSL
6
- # equivalents.
7
- SSH_CURVE_NAME_MAP = {
8
- "nistp256" => "prime256v1",
9
- "nistp384" => "secp384r1",
10
- "nistp521" => "secp521r1",
11
- }
12
-
13
- # Create a new `OpenSSL::PKey` from an SSH public or private key.
14
- #
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.
19
- #
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.
24
- #
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.
28
- #
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.
33
- #
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)
182
- if s =~ /\Assh-[a-z0-9-]+ /
183
- # WHOOP WHOOP prefixed key detected.
184
- s = s.split(" ")[1]
185
- else
186
- # Discard any comment, etc that might be lurking around
187
- s = s.split(" ")[0]
188
- end
189
-
190
- unless s =~ /\A[A-Za-z0-9\/+]+={0,2}\z/
191
- raise OpenSSL::PKey::PKeyError,
192
- "Invalid key encoding (not valid base64)"
193
- end
194
-
195
- parts = ssh_key_lv_decode(s.unpack("m").first)
196
-
197
- case parts.first
198
- when "ssh-rsa"
199
- OpenSSL::PKey::RSA.new.tap do |k|
200
- k.e = ssh_key_mpi_decode(parts[1])
201
- k.n = ssh_key_mpi_decode(parts[2])
202
- end
203
- when "ssh-dss"
204
- OpenSSL::PKey::DSA.new.tap do |k|
205
- k.p = ssh_key_mpi_decode(parts[1])
206
- k.q = ssh_key_mpi_decode(parts[2])
207
- k.g = ssh_key_mpi_decode(parts[3])
208
- end
209
- when /ecdsa-sha2-/
210
- begin
211
- OpenSSL::PKey::EC.new(SSH_CURVE_NAME_MAP[parts[1]]).tap do |k|
212
- k.public_key = OpenSSL::PKey::EC::Point.new(k.group, parts[2])
213
- end
214
- rescue TypeError
215
- raise OpenSSL::PKey::PKeyError.new,
216
- "Unknown curve identifier #{parts[1]}"
217
- end
218
- else
219
- raise OpenSSL::PKey::PKeyError,
220
- "Unknown key type #{parts.first.inspect}"
221
- end
222
- end
223
-
224
- # Take the base64 string and split it into its component parts.
225
- #
226
- def self.ssh_key_lv_decode(s, n = nil)
227
- rest = s
228
-
229
- [].tap do |parts|
230
- until rest == "" || (n && n <= 0)
231
- len, rest = rest.unpack("Na*")
232
- if len > rest.length
233
- raise OpenSSL::PKey::PKeyError,
234
- "Invalid LV-encoded string; wanted #{len} octets, but there's only #{rest.length} octets left"
235
- end
236
-
237
- elem, rest = rest.unpack("a#{len}a*")
238
- parts << elem
239
- n -= 1 if n
240
- end
241
-
242
- if rest != ""
243
- parts << rest
244
- end
245
- end
246
- end
247
-
248
- # Turn an SSH "MPI" (encoded arbitrary-length integer) string into a real
249
- # Ruby integer.
250
- #
251
- def self.ssh_key_mpi_decode(s)
252
- s.each_char.inject(0) { |i, c| i * 256 + c.ord }
253
- end
254
- end
1
+ require_relative './ssh_pkey'
@@ -0,0 +1,254 @@
1
+ require "openssl"
2
+
3
+ # Enhancements to the core asymmetric key handling.
4
+ module OpenSSL::PKey
5
+ # A mapping of the "SSH" names for various curves, to their OpenSSL
6
+ # equivalents.
7
+ SSH_CURVE_NAME_MAP = {
8
+ "nistp256" => "prime256v1",
9
+ "nistp384" => "secp384r1",
10
+ "nistp521" => "secp521r1",
11
+ }
12
+
13
+ # Create a new `OpenSSL::PKey` from an SSH public or private key.
14
+ #
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.
19
+ #
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.
24
+ #
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.
28
+ #
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.
33
+ #
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)
182
+ if s =~ /\Assh-[a-z0-9-]+ /
183
+ # WHOOP WHOOP prefixed key detected.
184
+ s = s.split(" ")[1]
185
+ else
186
+ # Discard any comment, etc that might be lurking around
187
+ s = s.split(" ")[0]
188
+ end
189
+
190
+ unless s =~ /\A[A-Za-z0-9\/+]+={0,2}\z/
191
+ raise OpenSSL::PKey::PKeyError,
192
+ "Invalid key encoding (not valid base64)"
193
+ end
194
+
195
+ parts = ssh_key_lv_decode(s.unpack("m").first)
196
+
197
+ case parts.first
198
+ when "ssh-rsa"
199
+ OpenSSL::PKey::RSA.new.tap do |k|
200
+ k.e = ssh_key_mpi_decode(parts[1])
201
+ k.n = ssh_key_mpi_decode(parts[2])
202
+ end
203
+ when "ssh-dss"
204
+ OpenSSL::PKey::DSA.new.tap do |k|
205
+ k.p = ssh_key_mpi_decode(parts[1])
206
+ k.q = ssh_key_mpi_decode(parts[2])
207
+ k.g = ssh_key_mpi_decode(parts[3])
208
+ end
209
+ when /ecdsa-sha2-/
210
+ begin
211
+ OpenSSL::PKey::EC.new(SSH_CURVE_NAME_MAP[parts[1]]).tap do |k|
212
+ k.public_key = OpenSSL::PKey::EC::Point.new(k.group, parts[2])
213
+ end
214
+ rescue TypeError
215
+ raise OpenSSL::PKey::PKeyError.new,
216
+ "Unknown curve identifier #{parts[1]}"
217
+ end
218
+ else
219
+ raise OpenSSL::PKey::PKeyError,
220
+ "Unknown key type #{parts.first.inspect}"
221
+ end
222
+ end
223
+
224
+ # Take the base64 string and split it into its component parts.
225
+ #
226
+ def self.ssh_key_lv_decode(s, n = nil)
227
+ rest = s
228
+
229
+ [].tap do |parts|
230
+ until rest == "" || (n && n <= 0)
231
+ len, rest = rest.unpack("Na*")
232
+ if len > rest.length
233
+ raise OpenSSL::PKey::PKeyError,
234
+ "Invalid LV-encoded string; wanted #{len} octets, but there's only #{rest.length} octets left"
235
+ end
236
+
237
+ elem, rest = rest.unpack("a#{len}a*")
238
+ parts << elem
239
+ n -= 1 if n
240
+ end
241
+
242
+ if rest != ""
243
+ parts << rest
244
+ end
245
+ end
246
+ end
247
+
248
+ # Turn an SSH "MPI" (encoded arbitrary-length integer) string into a real
249
+ # Ruby integer.
250
+ #
251
+ def self.ssh_key_mpi_decode(s)
252
+ s.each_char.inject(0) { |i, c| i * 256 + c.ord }
253
+ end
254
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openssl-additions
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Palmer
@@ -167,6 +167,7 @@ files:
167
167
  - lib/openssl/pkey.rb
168
168
  - lib/openssl/pkey/ec.rb
169
169
  - lib/openssl/pkey/rsa.rb
170
+ - lib/openssl/ssh_pkey.rb
170
171
  - lib/openssl/x509/certificate.rb
171
172
  - lib/openssl/x509/request.rb
172
173
  - lib/openssl/x509/spki.rb