ssh_data 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|