ssh_data 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: aaa29fd598724a11d13c32aa423fdb26944b5ed7d027b852de0fb3b6c8808f54
4
+ data.tar.gz: 6737375c449655f42d8f6a629065ad1e493f301e6ed118ca23a2d6b939b2516c
5
+ SHA512:
6
+ metadata.gz: 43afd3056dd763d3bfbeb44e98936a6a35a97172000772bb91107a516642c384b3398127e211a792193102351fe3ab6bafcf2d4e7674c7233f7610f743f80341
7
+ data.tar.gz: e0cc819489d6ac78f0cdf2e3fb9d22ded46dda75e11e6275d9e47a354d951ed753434a1cacc82377b4c1ef4c3469e8df73cc82e981062c2d0476513f7e9a287d
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 GitHub, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/lib/ssh_data.rb ADDED
@@ -0,0 +1,36 @@
1
+ require "openssl"
2
+ require "base64"
3
+
4
+ module SSHData
5
+ # Break down a key in OpenSSH authorized_keys format (see sshd(8) manual
6
+ # page).
7
+ #
8
+ # key - An OpenSSH formatted public key or certificate, including algo,
9
+ # base64 encoded key and optional comment.
10
+ #
11
+ # Returns an Array containing the algorithm String , the raw key or
12
+ # certificate String and the comment String or nil.
13
+ def key_parts(key)
14
+ algo, b64, comment = key.strip.split(" ", 3)
15
+ if algo.nil? || b64.nil?
16
+ raise DecodeError, "bad data format"
17
+ end
18
+
19
+ raw = begin
20
+ Base64.strict_decode64(b64)
21
+ rescue ArgumentError
22
+ raise DecodeError, "bad data format"
23
+ end
24
+
25
+ [algo, raw, comment]
26
+ end
27
+
28
+ extend self
29
+ end
30
+
31
+ require "ssh_data/version"
32
+ require "ssh_data/error"
33
+ require "ssh_data/certificate"
34
+ require "ssh_data/public_key"
35
+ require "ssh_data/private_key"
36
+ require "ssh_data/encoding"
@@ -0,0 +1,240 @@
1
+ require "securerandom"
2
+ require "ipaddr"
3
+
4
+ module SSHData
5
+ class Certificate
6
+ # Special values for valid_before and valid_after.
7
+ BEGINNING_OF_TIME = Time.at(0)
8
+ END_OF_TIME = Time.at((2**64)-1)
9
+
10
+ # Integer certificate types
11
+ TYPE_USER = 1
12
+ TYPE_HOST = 2
13
+
14
+ # Certificate algorithm identifiers
15
+ ALGO_RSA = "ssh-rsa-cert-v01@openssh.com"
16
+ ALGO_DSA = "ssh-dss-cert-v01@openssh.com"
17
+ ALGO_ECDSA256 = "ecdsa-sha2-nistp256-cert-v01@openssh.com"
18
+ ALGO_ECDSA384 = "ecdsa-sha2-nistp384-cert-v01@openssh.com"
19
+ ALGO_ECDSA521 = "ecdsa-sha2-nistp521-cert-v01@openssh.com"
20
+ ALGO_ED25519 = "ssh-ed25519-cert-v01@openssh.com"
21
+
22
+ ALGOS = [
23
+ ALGO_RSA, ALGO_DSA, ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521,
24
+ ALGO_ED25519
25
+ ]
26
+
27
+ CRITICAL_OPTION_FORCE_COMMAND = "force-command"
28
+ CRITICAL_OPTION_SOURCE_ADDRESS = "source-address"
29
+
30
+ attr_reader :algo, :nonce, :public_key, :serial, :type, :key_id,
31
+ :valid_principals, :valid_after, :valid_before,
32
+ :critical_options, :extensions, :reserved, :ca_key, :signature
33
+
34
+ # Parse an OpenSSH certificate in authorized_keys format (see sshd(8) manual
35
+ # page).
36
+ #
37
+ # cert - An OpenSSH formatted certificate, including key algo,
38
+ # base64 encoded key and optional comment.
39
+ # unsafe_no_verify: - Bool of whether to skip verifying certificate signature
40
+ # (Default false)
41
+ #
42
+ # Returns a Certificate instance.
43
+ def self.parse_openssh(cert, unsafe_no_verify: false)
44
+ algo, raw, _ = SSHData.key_parts(cert)
45
+ parsed = parse_rfc4253(raw, unsafe_no_verify: unsafe_no_verify)
46
+
47
+ if parsed.algo != algo
48
+ raise DecodeError, "algo mismatch: #{parsed.algo.inspect}!=#{algo.inspect}"
49
+ end
50
+
51
+ parsed
52
+ end
53
+
54
+ # Deprecated
55
+ singleton_class.send(:alias_method, :parse, :parse_openssh)
56
+
57
+ # Parse an RFC 4253 binary SSH certificate.
58
+ #
59
+ # cert - A RFC 4253 binary certificate String.
60
+ # unsafe_no_verify: - Bool of whether to skip verifying certificate
61
+ # signature (Default false)
62
+ #
63
+ # Returns a Certificate instance.
64
+ def self.parse_rfc4253(raw, unsafe_no_verify: false)
65
+ data, read = Encoding.decode_certificate(raw)
66
+
67
+ if read != raw.bytesize
68
+ raise DecodeError, "unexpected trailing data"
69
+ end
70
+
71
+ # Parse data into better types, where possible.
72
+ public_key = PublicKey.from_data(data.delete(:public_key))
73
+ ca_key = PublicKey.from_data(data.delete(:signature_key))
74
+
75
+ new(**data.merge(public_key: public_key, ca_key: ca_key)).tap do |cert|
76
+ raise VerifyError unless unsafe_no_verify || cert.verify
77
+ end
78
+ end
79
+
80
+ # Intialize a new Certificate instance.
81
+ #
82
+ # algo: - The certificate's String algorithm id (one of ALGO_RSA,
83
+ # ALGO_DSA, ALGO_ECDSA256, ALGO_ECDSA384, ALGO_ECDSA521,
84
+ # or ALGO_ED25519)
85
+ # nonce: - The certificate's String nonce field.
86
+ # public_key: - The certificate's public key as an PublicKey::Base
87
+ # subclass instance.
88
+ # serial: - The certificate's Integer serial field.
89
+ # type: - The certificate's Integer type field (one of TYPE_USER
90
+ # or TYPE_HOST).
91
+ # key_id: - The certificate's String key_id field.
92
+ # valid_principals: - The Array of Strings valid_principles field from the
93
+ # certificate.
94
+ # valid_after: - The certificate's Time valid_after field.
95
+ # valid_before: - The certificate's Time valid_before field.
96
+ # critical_options: - The Hash critical_options field from the certificate.
97
+ # extensions: - The Hash extensions field from the certificate.
98
+ # reserved: - The certificate's String reserved field.
99
+ # ca_key: - The issuing CA's public key as a PublicKey::Base
100
+ # subclass instance.
101
+ # signature: - The certificate's String signature field.
102
+ #
103
+ # Returns nothing.
104
+ def initialize(public_key:, key_id:, algo: nil, nonce: nil, serial: 0, type: TYPE_USER, valid_principals: [], valid_after: BEGINNING_OF_TIME, valid_before: END_OF_TIME, critical_options: {}, extensions: {}, reserved: "", ca_key: nil, signature: "")
105
+ @algo = algo || Encoding::CERT_ALGO_BY_PUBLIC_KEY_ALGO[public_key.algo]
106
+ @nonce = nonce || SecureRandom.random_bytes(32)
107
+ @public_key = public_key
108
+ @serial = serial
109
+ @type = type
110
+ @key_id = key_id
111
+ @valid_principals = valid_principals
112
+ @valid_after = valid_after
113
+ @valid_before = valid_before
114
+ @critical_options = critical_options
115
+ @extensions = extensions
116
+ @reserved = reserved
117
+ @ca_key = ca_key
118
+ @signature = signature
119
+ end
120
+
121
+ # OpenSSH certificate in authorized_keys format (see sshd(8) manual page).
122
+ #
123
+ # comment - Optional String comment to append.
124
+ #
125
+ # Returns a String key.
126
+ def openssh(comment: nil)
127
+ [algo, Base64.strict_encode64(rfc4253), comment].compact.join(" ")
128
+ end
129
+
130
+ # RFC4253 binary encoding of the certificate.
131
+ #
132
+ # Returns a binary String.
133
+ def rfc4253
134
+ Encoding.encode_fields(
135
+ [:string, algo],
136
+ [:string, nonce],
137
+ [:raw, public_key_without_algo],
138
+ [:uint64, serial],
139
+ [:uint32, type],
140
+ [:string, key_id],
141
+ [:list, valid_principals],
142
+ [:time, valid_after],
143
+ [:time, valid_before],
144
+ [:options, critical_options],
145
+ [:options, extensions],
146
+ [:string, reserved],
147
+ [:string, ca_key.rfc4253],
148
+ [:string, signature],
149
+ )
150
+ end
151
+
152
+ # Sign this certificate with a private key.
153
+ #
154
+ # private_key - An SSHData::PrivateKey::Base subclass instance.
155
+ # algo: - Optionally specify the signature algorithm to use.
156
+ #
157
+ # Returns nothing.
158
+ def sign(private_key, algo: nil)
159
+ @ca_key = private_key.public_key
160
+ @signature = private_key.sign(signed_data, algo: algo)
161
+ end
162
+
163
+ # Verify the certificate's signature.
164
+ #
165
+ # Returns boolean.
166
+ def verify
167
+ ca_key.verify(signed_data, signature)
168
+ end
169
+
170
+ # The force-command critical option, if present.
171
+ #
172
+ # Returns a String or nil.
173
+ def force_command
174
+ case value = critical_options[CRITICAL_OPTION_FORCE_COMMAND]
175
+ when String, NilClass
176
+ value
177
+ else
178
+ raise DecodeError, "bad force-request"
179
+ end
180
+ end
181
+
182
+ # The source-address critical option, if present.
183
+ #
184
+ # Returns an Array of IPAddr instances or nil.
185
+ def source_address
186
+ return @source_address if defined?(@source_address)
187
+
188
+ value = critical_options[CRITICAL_OPTION_SOURCE_ADDRESS]
189
+
190
+ @source_address = case value
191
+ when String
192
+ value.split(",").map do |str_addr|
193
+ begin
194
+ IPAddr.new(str_addr.strip)
195
+ rescue IPAddr::InvalidAddressError => e
196
+ raise DecodeError, "bad source-address: #{e.message}"
197
+ end
198
+ end
199
+ when NilClass
200
+ nil
201
+ else
202
+ raise DecodeError, "bad source-address"
203
+ end
204
+ end
205
+
206
+ # Check if the given IP address is allowed for use with this certificate.
207
+ #
208
+ # address - A String IP address.
209
+ #
210
+ # Returns boolean.
211
+ def allowed_source_address?(address)
212
+ return true if source_address.nil?
213
+ parsed_addr = IPAddr.new(address)
214
+ source_address.any? { |a| a.include?(parsed_addr) }
215
+ rescue IPAddr::InvalidAddressError
216
+ return false
217
+ end
218
+
219
+ private
220
+
221
+ # The portion of the certificate over which the signature is calculated.
222
+ #
223
+ # Returns a binary String.
224
+ def signed_data
225
+ siglen = self.signature.bytesize + 4
226
+ rfc4253.byteslice(0...-siglen)
227
+ end
228
+
229
+ # Helper for getting the RFC4253 encoded public key with the first field
230
+ # (the algorithm) stripped off.
231
+ #
232
+ # Returns a String.
233
+ def public_key_without_algo
234
+ key = public_key.rfc4253
235
+ _, algo_len = Encoding.decode_string(key)
236
+ key.byteslice(algo_len..-1)
237
+ end
238
+
239
+ end
240
+ end
@@ -0,0 +1,666 @@
1
+ module SSHData
2
+ module Encoding
3
+ # Fields in an OpenSSL private key
4
+ # https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key
5
+ OPENSSH_PRIVATE_KEY_MAGIC = "openssh-key-v1\x00"
6
+ OPENSSH_PRIVATE_KEY_FIELDS = [
7
+ [:ciphername, :string],
8
+ [:kdfname, :string],
9
+ [:kdfoptions, :string],
10
+ [:nkeys, :uint32],
11
+ ]
12
+
13
+ # Fields in an RSA private key
14
+ RSA_PRIVATE_KEY_FIELDS = [
15
+ [:n, :mpint],
16
+ [:e, :mpint],
17
+ [:d, :mpint],
18
+ [:iqmp, :mpint],
19
+ [:p, :mpint],
20
+ [:q, :mpint],
21
+ ]
22
+
23
+ # Fields in a DSA private key
24
+ DSA_PRIVATE_KEY_FIELDS = [
25
+ [:p, :mpint],
26
+ [:q, :mpint],
27
+ [:g, :mpint],
28
+ [:y, :mpint],
29
+ [:x, :mpint]
30
+ ]
31
+
32
+ # Fields in a ECDSA private key
33
+ ECDSA_PRIVATE_KEY_FIELDS = [
34
+ [:curve, :string],
35
+ [:public_key, :string],
36
+ [:private_key, :mpint],
37
+ ]
38
+
39
+ # Fields in a ED25519 private key
40
+ ED25519_PRIVATE_KEY_FIELDS = [
41
+ [:pk, :string],
42
+ [:sk, :string]
43
+ ]
44
+
45
+ # Fields in an RSA public key
46
+ RSA_KEY_FIELDS = [
47
+ [:e, :mpint],
48
+ [:n, :mpint]
49
+ ]
50
+
51
+ # Fields in a DSA public key
52
+ DSA_KEY_FIELDS = [
53
+ [:p, :mpint],
54
+ [:q, :mpint],
55
+ [:g, :mpint],
56
+ [:y, :mpint]
57
+ ]
58
+
59
+ # Fields in an ECDSA public key
60
+ ECDSA_KEY_FIELDS = [
61
+ [:curve, :string],
62
+ [:public_key, :string]
63
+ ]
64
+
65
+ # Fields in a ED25519 public key
66
+ ED25519_KEY_FIELDS = [
67
+ [:pk, :string]
68
+ ]
69
+
70
+ PUBLIC_KEY_ALGO_BY_CERT_ALGO = {
71
+ Certificate::ALGO_RSA => PublicKey::ALGO_RSA,
72
+ Certificate::ALGO_DSA => PublicKey::ALGO_DSA,
73
+ Certificate::ALGO_ECDSA256 => PublicKey::ALGO_ECDSA256,
74
+ Certificate::ALGO_ECDSA384 => PublicKey::ALGO_ECDSA384,
75
+ Certificate::ALGO_ECDSA521 => PublicKey::ALGO_ECDSA521,
76
+ Certificate::ALGO_ED25519 => PublicKey::ALGO_ED25519,
77
+ }
78
+
79
+ CERT_ALGO_BY_PUBLIC_KEY_ALGO = {
80
+ PublicKey::ALGO_RSA => Certificate::ALGO_RSA,
81
+ PublicKey::ALGO_DSA => Certificate::ALGO_DSA,
82
+ PublicKey::ALGO_ECDSA256 => Certificate::ALGO_ECDSA256,
83
+ PublicKey::ALGO_ECDSA384 => Certificate::ALGO_ECDSA384,
84
+ PublicKey::ALGO_ECDSA521 => Certificate::ALGO_ECDSA521,
85
+ PublicKey::ALGO_ED25519 => Certificate::ALGO_ED25519,
86
+ }
87
+
88
+ KEY_FIELDS_BY_PUBLIC_KEY_ALGO = {
89
+ PublicKey::ALGO_RSA => RSA_KEY_FIELDS,
90
+ PublicKey::ALGO_DSA => DSA_KEY_FIELDS,
91
+ PublicKey::ALGO_ECDSA256 => ECDSA_KEY_FIELDS,
92
+ PublicKey::ALGO_ECDSA384 => ECDSA_KEY_FIELDS,
93
+ PublicKey::ALGO_ECDSA521 => ECDSA_KEY_FIELDS,
94
+ PublicKey::ALGO_ED25519 => ED25519_KEY_FIELDS,
95
+ }
96
+
97
+ KEY_FIELDS_BY_PRIVATE_KEY_ALGO = {
98
+ PublicKey::ALGO_RSA => RSA_PRIVATE_KEY_FIELDS,
99
+ PublicKey::ALGO_DSA => DSA_PRIVATE_KEY_FIELDS,
100
+ PublicKey::ALGO_ECDSA256 => ECDSA_PRIVATE_KEY_FIELDS,
101
+ PublicKey::ALGO_ECDSA384 => ECDSA_PRIVATE_KEY_FIELDS,
102
+ PublicKey::ALGO_ECDSA521 => ECDSA_PRIVATE_KEY_FIELDS,
103
+ PublicKey::ALGO_ED25519 => ED25519_PRIVATE_KEY_FIELDS,
104
+ }
105
+
106
+ # Get the type from a PEM encoded blob.
107
+ #
108
+ # pem - A PEM encoded String.
109
+ #
110
+ # Returns a String PEM type.
111
+ def pem_type(pem)
112
+ head = pem.split("\n", 2).first.strip
113
+
114
+ head_prefix = "-----BEGIN "
115
+ head_suffix = "-----"
116
+
117
+ unless head.start_with?(head_prefix) && head.end_with?(head_suffix)
118
+ raise DecodeError, "bad PEM encoding"
119
+ end
120
+
121
+ type_size = head.bytesize - head_prefix.bytesize - head_suffix.bytesize
122
+
123
+ head.byteslice(head_prefix.bytesize, type_size)
124
+ end
125
+
126
+ # Get the raw data from a PEM encoded blob.
127
+ #
128
+ # pem - The PEM encoded String to decode.
129
+ # type - The String PEM type we're expecting.
130
+ #
131
+ # Returns the decoded String.
132
+ def decode_pem(pem, type)
133
+ lines = pem.split("\n").map(&:strip)
134
+
135
+ unless lines.shift == "-----BEGIN #{type}-----"
136
+ raise DecodeError, "bad PEM header"
137
+ end
138
+
139
+ unless lines.pop == "-----END #{type}-----"
140
+ raise DecodeError, "bad PEM footer"
141
+ end
142
+
143
+ begin
144
+ Base64.strict_decode64(lines.join)
145
+ rescue ArgumentError
146
+ raise DecodeError, "bad PEM data"
147
+ end
148
+ end
149
+
150
+ # Decode an OpenSSH private key.
151
+ #
152
+ # raw - The binary String private key.
153
+ #
154
+ # Returns an Array containing a Hash describing the private key and the
155
+ # Integer number of bytes read.
156
+ def decode_openssh_private_key(raw)
157
+ total_read = 0
158
+
159
+ magic = raw.byteslice(total_read, OPENSSH_PRIVATE_KEY_MAGIC.bytesize)
160
+ unless magic == OPENSSH_PRIVATE_KEY_MAGIC
161
+ raise DecodeError, "bad OpenSSH private key"
162
+ end
163
+ total_read += OPENSSH_PRIVATE_KEY_MAGIC.bytesize
164
+
165
+ data, read = decode_fields(raw, OPENSSH_PRIVATE_KEY_FIELDS, total_read)
166
+ total_read += read
167
+
168
+ # TODO: add support for encrypted private keys
169
+ unless data[:ciphername] == "none" && data[:kdfname] == "none"
170
+ raise DecryptError, "cannot decode encrypted private keys"
171
+ end
172
+
173
+ data[:public_keys], read = decode_n_strings(raw, total_read, data[:nkeys])
174
+ total_read += read
175
+
176
+ privs, read = decode_string(raw, total_read)
177
+ total_read += read
178
+
179
+ privs_read = 0
180
+
181
+ data[:checkint1], read = decode_uint32(privs, privs_read)
182
+ privs_read += read
183
+
184
+ data[:checkint2], read = decode_uint32(privs, privs_read)
185
+ privs_read += read
186
+
187
+ unless data[:checkint1] == data[:checkint2]
188
+ raise DecryptError, "bad private key checksum"
189
+ end
190
+
191
+ data[:private_keys] = data[:nkeys].times.map do
192
+ algo, read = decode_string(privs, privs_read)
193
+ privs_read += read
194
+
195
+ unless fields = KEY_FIELDS_BY_PRIVATE_KEY_ALGO[algo]
196
+ raise AlgorithmError, "unknown algorithm: #{algo.inspect}"
197
+ end
198
+
199
+ priv_data, read = decode_fields(privs, fields, privs_read)
200
+ privs_read += read
201
+
202
+ comment, read = decode_string(privs, privs_read)
203
+ privs_read += read
204
+
205
+ priv_data.merge(algo: algo, comment: comment)
206
+ end
207
+
208
+ # padding at end is bytes 1, 2, 3, 4, etc...
209
+ data[:padding] = privs.byteslice(privs_read..-1)
210
+ unless data[:padding].bytes.each_with_index.all? { |b, i| b == (i + 1) % 255 }
211
+ raise DecodeError, "bad padding: #{data[:padding].inspect}"
212
+ end
213
+
214
+ [data, total_read]
215
+ end
216
+
217
+ # Decode the signature.
218
+ #
219
+ # raw - The binary String signature as described by RFC4253 section 6.6.
220
+ # offset - Integer number of bytes into `raw` at which we should start
221
+ # reading.
222
+ #
223
+ # Returns an Array containing the decoded algorithm String, the decoded binary
224
+ # signature String, and the Integer number of bytes read.
225
+ def decode_signature(raw, offset=0)
226
+ total_read = 0
227
+
228
+ algo, read = decode_string(raw, offset + total_read)
229
+ total_read += read
230
+
231
+ sig, read = decode_string(raw, offset + total_read)
232
+ total_read += read
233
+
234
+ [algo, sig, total_read]
235
+ end
236
+
237
+ # Encoding a signature.
238
+ #
239
+ # algo - The String signature algorithm.
240
+ # signature - The String signature blob.
241
+ #
242
+ # Returns an encoded String.
243
+ def encode_signature(algo, signature)
244
+ encode_string(algo) + encode_string(signature)
245
+ end
246
+
247
+ # Decode the fields in a public key.
248
+ #
249
+ # raw - Binary String public key as described by RFC4253 section 6.6.
250
+ # algo - String public key algorithm identifier (optional).
251
+ # offset - Integer number of bytes into `raw` at which we should start
252
+ # reading.
253
+ #
254
+ # Returns an Array containing a Hash describing the public key and the
255
+ # Integer number of bytes read.
256
+ def decode_public_key(raw, offset=0, algo=nil)
257
+ total_read = 0
258
+
259
+ if algo.nil?
260
+ algo, read = decode_string(raw, offset + total_read)
261
+ total_read += read
262
+ end
263
+
264
+ unless fields = KEY_FIELDS_BY_PUBLIC_KEY_ALGO[algo]
265
+ raise AlgorithmError, "unknown algorithm: #{algo.inspect}"
266
+ end
267
+
268
+ data, read = decode_fields(raw, fields, offset + total_read)
269
+ total_read += read
270
+
271
+ data[:algo] = algo
272
+
273
+ [data, total_read]
274
+ end
275
+
276
+ # Decode the fields in a public key encoded as an SSH string.
277
+ #
278
+ # raw - Binary public key as described by RFC4253 section 6.6 wrapped in
279
+ # an SSH string..
280
+ # algo - String public key algorithm identifier (optional).
281
+ # offset - Integer number of bytes into `raw` at which we should start
282
+ # reading.
283
+ #
284
+ # Returns an Array containing a Hash describing the public key and the
285
+ # Integer number of bytes read.
286
+ def decode_string_public_key(raw, offset=0, algo=nil)
287
+ key_raw, str_read = decode_string(raw, offset)
288
+ key, cert_read = decode_public_key(key_raw, 0, algo)
289
+
290
+ if cert_read != key_raw.bytesize
291
+ raise DecodeError, "unexpected trailing data"
292
+ end
293
+
294
+ [key, str_read]
295
+ end
296
+
297
+ # Decode the fields in a certificate.
298
+ #
299
+ # raw - Binary String certificate as described by RFC4253 section 6.6.
300
+ # offset - Integer number of bytes into `raw` at which we should start
301
+ # reading.
302
+ #
303
+ # Returns an Array containing a Hash describing the certificate and the
304
+ # Integer number of bytes read.
305
+ def decode_certificate(raw, offset=0)
306
+ total_read = 0
307
+
308
+ algo, read = decode_string(raw, offset + total_read)
309
+ total_read += read
310
+
311
+ unless key_algo = PUBLIC_KEY_ALGO_BY_CERT_ALGO[algo]
312
+ raise AlgorithmError, "unknown algorithm: #{algo.inspect}"
313
+ end
314
+
315
+ data, read = decode_fields(raw, [
316
+ [:nonce, :string],
317
+ [:public_key, :public_key, key_algo],
318
+ [:serial, :uint64],
319
+ [:type, :uint32],
320
+ [:key_id, :string],
321
+ [:valid_principals, :list],
322
+ [:valid_after, :time],
323
+ [:valid_before, :time],
324
+ [:critical_options, :options],
325
+ [:extensions, :options],
326
+ [:reserved, :string],
327
+ [:signature_key, :string_public_key],
328
+ [:signature, :string],
329
+ ], offset + total_read)
330
+ total_read += read
331
+
332
+ data[:algo] = algo
333
+
334
+ [data, total_read]
335
+ end
336
+
337
+ # Decode all of the given fields from raw.
338
+ #
339
+ # raw - A binary String.
340
+ # fields - An Array of Arrays, each containing a symbol describing the field
341
+ # and a Symbol describing the type of the field (:mpint, :string,
342
+ # :uint64, or :uint32).
343
+ # offset - The offset into raw at which to read (default 0).
344
+ #
345
+ # Returns an Array containing a Hash mapping the provided field keys to the
346
+ # decoded values and the Integer number of bytes read.
347
+ def decode_fields(raw, fields, offset=0)
348
+ hash = {}
349
+ total_read = 0
350
+
351
+ fields.each do |key, type, *args|
352
+ hash[key], read = case type
353
+ when :string
354
+ decode_string(raw, offset + total_read, *args)
355
+ when :list
356
+ decode_list(raw, offset + total_read, *args)
357
+ when :mpint
358
+ decode_mpint(raw, offset + total_read, *args)
359
+ when :time
360
+ decode_time(raw, offset + total_read, *args)
361
+ when :uint64
362
+ decode_uint64(raw, offset + total_read, *args)
363
+ when :uint32
364
+ decode_uint32(raw, offset + total_read, *args)
365
+ when :public_key
366
+ decode_public_key(raw, offset + total_read, *args)
367
+ when :string_public_key
368
+ decode_string_public_key(raw, offset + total_read, *args)
369
+ when :options
370
+ decode_options(raw, offset + total_read, *args)
371
+ else
372
+ raise DecodeError
373
+ end
374
+ total_read += read
375
+ end
376
+
377
+ [hash, total_read]
378
+ end
379
+
380
+ # Encode the series of fiends into a binary string.
381
+ #
382
+ # fields - A series of Arrays, each containing a Symbol type and a value to
383
+ # encode.
384
+ #
385
+ # Returns a binary String.
386
+ def encode_fields(*fields)
387
+ fields.map do |type, value|
388
+ case type
389
+ when :raw
390
+ value
391
+ when :string
392
+ encode_string(value)
393
+ when :list
394
+ encode_list(value)
395
+ when :mpint
396
+ encode_mpint(value)
397
+ when :time
398
+ encode_time(value)
399
+ when :uint64
400
+ encode_uint64(value)
401
+ when :uint32
402
+ encode_uint32(value)
403
+ when :options
404
+ encode_options(value)
405
+ else
406
+ raise DecodeError, "bad type: #{type}"
407
+ end
408
+ end.join
409
+ end
410
+
411
+ # Read a string out of the provided raw data.
412
+ #
413
+ # raw - A binary String.
414
+ # offset - The offset into raw at which to read (default 0).
415
+ #
416
+ # Returns an Array including the decoded String and the Integer number of
417
+ # bytes read.
418
+ def decode_string(raw, offset=0)
419
+ if raw.bytesize < offset + 4
420
+ raise DecodeError, "data too short"
421
+ end
422
+
423
+ size_s = raw.byteslice(offset, 4)
424
+
425
+ size = size_s.unpack("L>").first
426
+
427
+ if raw.bytesize < offset + 4 + size
428
+ raise DecodeError, "data too short"
429
+ end
430
+
431
+ string = raw.byteslice(offset + 4, size)
432
+
433
+ [string, 4 + size]
434
+ end
435
+
436
+ # Encoding a string.
437
+ #
438
+ # value - The String value to encode.
439
+ #
440
+ # Returns an encoded representation of the String.
441
+ def encode_string(value)
442
+ [value.bytesize, value].pack("L>A*")
443
+ end
444
+
445
+ # Read a series of strings out of the provided raw data.
446
+ #
447
+ # raw - A binary String.
448
+ # offset - The offset into raw at which to read (default 0).
449
+ #
450
+ # Returns an Array including the Array of decoded Strings and the Integer
451
+ # number of bytes read.
452
+ def decode_list(raw, offset=0)
453
+ list_raw, str_read = decode_string(raw, offset)
454
+
455
+ list_read = 0
456
+ list = []
457
+
458
+ while list_raw.bytesize > list_read
459
+ value, read = decode_string(list_raw, list_read)
460
+ list << value
461
+ list_read += read
462
+ end
463
+
464
+ if list_read != list_raw.bytesize
465
+ raise DecodeError, "bad strings list"
466
+ end
467
+
468
+ [list, str_read]
469
+ end
470
+
471
+ # Encode a list of strings.
472
+ #
473
+ # value - The Array of Strings to encode.
474
+ #
475
+ # Returns an encoded representation of the list.
476
+ def encode_list(value)
477
+ encode_string(value.map { |s| encode_string(s) }.join)
478
+ end
479
+
480
+ # Read a multi-precision integer from the provided raw data.
481
+ #
482
+ # raw - A binary String.
483
+ # offset - The offset into raw at which to read (default 0).
484
+ #
485
+ # Returns an Array including the decoded mpint as an OpenSSL::BN and the
486
+ # Integer number of bytes read.
487
+ def decode_mpint(raw, offset=0)
488
+ if raw.bytesize < offset + 4
489
+ raise DecodeError, "data too short"
490
+ end
491
+
492
+ str_size_s = raw.byteslice(offset, 4)
493
+ str_size = str_size_s.unpack("L>").first
494
+ mpi_size = str_size + 4
495
+
496
+ if raw.bytesize < offset + mpi_size
497
+ raise DecodeError, "data too short"
498
+ end
499
+
500
+ mpi_s = raw.slice(offset, mpi_size)
501
+
502
+ # This calls OpenSSL's BN_mpi2bn() function. As far as I can tell, this
503
+ # matches up with with MPI type defined in RFC4251 Section 5 with the
504
+ # exception that OpenSSL doesn't enforce minimal length. We could enforce
505
+ # this ourselves, but it doesn't seem worth the added complexity.
506
+ mpi = OpenSSL::BN.new(mpi_s, 0)
507
+
508
+ [mpi, mpi_size]
509
+ end
510
+
511
+ # Encode a BN as an mpint.
512
+ #
513
+ # value - The OpenSSL::BN value to encode.
514
+ #
515
+ # Returns an encoded representation of the BN.
516
+ def encode_mpint(value)
517
+ value.to_s(0)
518
+ end
519
+
520
+ # Read a time from the provided raw data.
521
+ #
522
+ # raw - A binary String.
523
+ # offset - The offset into raw at which to read (default 0).
524
+ #
525
+ # Returns an Array including the decoded Time and the Integer number of
526
+ # bytes read.
527
+ def decode_time(raw, offset=0)
528
+ time_raw, read = decode_uint64(raw, offset)
529
+ [Time.at(time_raw), read]
530
+ end
531
+
532
+ # Encode a time.
533
+ #
534
+ # value - The Time value to encode.
535
+ #
536
+ # Returns an encoded representation of the Time.
537
+ def encode_time(value)
538
+ encode_uint64(value.to_i)
539
+ end
540
+
541
+ # Read the specified number of strings out of the provided raw data.
542
+ #
543
+ # raw - A binary String.
544
+ # offset - The offset into raw at which to read (default 0).
545
+ # n - The Integer number of Strings to read.
546
+ #
547
+ # Returns an Array including the Array of decoded Strings and the Integer
548
+ # number of bytes read.
549
+ def decode_n_strings(raw, offset=0, n)
550
+ total_read = 0
551
+ strs = []
552
+
553
+ n.times do |i|
554
+ strs[i], read = decode_string(raw, offset + total_read)
555
+ total_read += read
556
+ end
557
+
558
+ [strs, total_read]
559
+ end
560
+
561
+ # Read a series of key/value pairs out of the provided raw data.
562
+ #
563
+ # raw - A binary String.
564
+ # offset - The offset into raw at which to read (default 0).
565
+ #
566
+ # Returns an Array including the Hash of decoded keys/values and the Integer
567
+ # number of bytes read.
568
+ def decode_options(raw, offset=0)
569
+ opts_raw, str_read = decode_string(raw, offset)
570
+
571
+ opts_read = 0
572
+ opts = {}
573
+
574
+ while opts_raw.bytesize > opts_read
575
+ key, read = decode_string(opts_raw, opts_read)
576
+ opts_read += read
577
+
578
+ value_raw, read = decode_string(opts_raw, opts_read)
579
+ opts_read += read
580
+
581
+ if value_raw.bytesize > 0
582
+ opts[key], read = decode_string(value_raw)
583
+ if read != value_raw.bytesize
584
+ raise DecodeError, "bad options data"
585
+ end
586
+ else
587
+ opts[key] = true
588
+ end
589
+ end
590
+
591
+ if opts_read != opts_raw.bytesize
592
+ raise DecodeError, "bad options"
593
+ end
594
+
595
+ [opts, str_read]
596
+ end
597
+
598
+ # Encode series of key/value pairs.
599
+ #
600
+ # value - The Hash value to encode.
601
+ #
602
+ # Returns an encoded representation of the Hash.
603
+ def encode_options(value)
604
+ opts_raw = value.reduce("") do |encoded, (key, value)|
605
+ value_str = value == true ? "" : encode_string(value)
606
+ encoded + encode_string(key) + encode_string(value_str)
607
+ end
608
+
609
+ encode_string(opts_raw)
610
+ end
611
+
612
+ # Read a uint64 from the provided raw data.
613
+ #
614
+ # raw - A binary String.
615
+ # offset - The offset into raw at which to read (default 0).
616
+ #
617
+ # Returns an Array including the decoded uint64 as an Integer and the
618
+ # Integer number of bytes read.
619
+ def decode_uint64(raw, offset=0)
620
+ if raw.bytesize < offset + 8
621
+ raise DecodeError, "data too short"
622
+ end
623
+
624
+ uint64 = raw.byteslice(offset, 8).unpack("Q>").first
625
+
626
+ [uint64, 8]
627
+ end
628
+
629
+ # Encoding an integer as a uint64.
630
+ #
631
+ # value - The Integer value to encode.
632
+ #
633
+ # Returns an encoded representation of the value.
634
+ def encode_uint64(value)
635
+ [value].pack("Q>")
636
+ end
637
+
638
+ # Read a uint32 from the provided raw data.
639
+ #
640
+ # raw - A binary String.
641
+ # offset - The offset into raw at which to read (default 0).
642
+ #
643
+ # Returns an Array including the decoded uint32 as an Integer and the
644
+ # Integer number of bytes read.
645
+ def decode_uint32(raw, offset=0)
646
+ if raw.bytesize < offset + 4
647
+ raise DecodeError, "data too short"
648
+ end
649
+
650
+ uint32 = raw.byteslice(offset, 4).unpack("L>").first
651
+
652
+ [uint32, 4]
653
+ end
654
+
655
+ # Encoding an integer as a uint32.
656
+ #
657
+ # value - The Integer value to encode.
658
+ #
659
+ # Returns an encoded representation of the value.
660
+ def encode_uint32(value)
661
+ [value].pack("L>")
662
+ end
663
+
664
+ extend self
665
+ end
666
+ end