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 +7 -0
- data/LICENSE.md +21 -0
- data/lib/ssh_data.rb +36 -0
- data/lib/ssh_data/certificate.rb +240 -0
- data/lib/ssh_data/encoding.rb +666 -0
- data/lib/ssh_data/error.rb +7 -0
- data/lib/ssh_data/private_key.rb +73 -0
- data/lib/ssh_data/private_key/base.rb +39 -0
- data/lib/ssh_data/private_key/dsa.rb +75 -0
- data/lib/ssh_data/private_key/ecdsa.rb +95 -0
- data/lib/ssh_data/private_key/ed25519.rb +68 -0
- data/lib/ssh_data/private_key/rsa.rb +106 -0
- data/lib/ssh_data/public_key.rb +78 -0
- data/lib/ssh_data/public_key/base.rb +71 -0
- data/lib/ssh_data/public_key/dsa.rb +122 -0
- data/lib/ssh_data/public_key/ecdsa.rb +151 -0
- data/lib/ssh_data/public_key/ed25519.rb +74 -0
- data/lib/ssh_data/public_key/rsa.rb +79 -0
- data/lib/ssh_data/version.rb +3 -0
- metadata +116 -0
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
|