ssh_data 1.1.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 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