octokey 0.1.pre.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/octokey/buffer.rb +179 -0
- data/lib/octokey.rb +366 -0
- metadata +46 -0
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'base64'
|
2
|
+
class Octokey
|
3
|
+
class Buffer
|
4
|
+
attr_accessor :buffer, :pos
|
5
|
+
|
6
|
+
# to avoid DOS caused by duplicating enourmous buffers,
|
7
|
+
# we limit the maximum size of any string stored to 100k
|
8
|
+
MAX_STRING_SIZE = 100 * 1024
|
9
|
+
|
10
|
+
def self.from_raw(raw = "")
|
11
|
+
ret = new
|
12
|
+
ret.buffer = raw.dup
|
13
|
+
ret.buffer.force_encoding('BINARY') if ret.buffer.respond_to?(:force_encoding)
|
14
|
+
ret
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(string = "")
|
18
|
+
self.buffer = Base64.decode64(string || "")
|
19
|
+
self.pos = 0
|
20
|
+
buffer.force_encoding('BINARY') if @buffer.respond_to?(:force_encoding)
|
21
|
+
end
|
22
|
+
|
23
|
+
def raw
|
24
|
+
buffer
|
25
|
+
end
|
26
|
+
|
27
|
+
def empty?
|
28
|
+
buffer.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
Base64.encode64(buffer).gsub("\n", "")
|
33
|
+
end
|
34
|
+
|
35
|
+
def <<(bytes)
|
36
|
+
buffer << bytes
|
37
|
+
end
|
38
|
+
|
39
|
+
def scan(n)
|
40
|
+
ret, buf = [buffer[0...n], buffer[n..-1]]
|
41
|
+
if ret.size < n || !buf
|
42
|
+
raise InvalidBuffer, "Tried to read beyond end of buffer"
|
43
|
+
end
|
44
|
+
self.buffer = buf
|
45
|
+
ret
|
46
|
+
end
|
47
|
+
|
48
|
+
def add_uint8(x)
|
49
|
+
raise InvalidBuffer, "Invalid uint8: #{x}" if x < 0 || x >= 2 ** 8
|
50
|
+
buffer << [x].pack("C")
|
51
|
+
end
|
52
|
+
|
53
|
+
def scan_uint8
|
54
|
+
scan(1).unpack("C").first
|
55
|
+
end
|
56
|
+
|
57
|
+
def add_uint32(x)
|
58
|
+
raise InvalidBuffer, "Invalid uint32: #{x}" if x < 0 || x >= 2 ** 32
|
59
|
+
buffer << [x].pack("N")
|
60
|
+
end
|
61
|
+
|
62
|
+
def scan_uint32
|
63
|
+
scan(4).unpack("N").first
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_uint64(x)
|
67
|
+
raise InvalidBuffer, "Invalid uint64: #{x}" if x < 0 || x >= 2 ** 64
|
68
|
+
add_uint32(x >> 32 & 0xffff_ffff)
|
69
|
+
add_uint32(x & 0xffff_ffff)
|
70
|
+
end
|
71
|
+
|
72
|
+
def scan_uint64
|
73
|
+
(scan_uint32 << 32) + scan_uint32
|
74
|
+
end
|
75
|
+
|
76
|
+
def add_uint128(x)
|
77
|
+
raise InvalidBuffer, "Invalid uint128: #{x}" if x < 0 || x >= 2 ** 128
|
78
|
+
add_uint64(x >> 64 & 0xffff_ffff_ffff_ffff)
|
79
|
+
add_uint64(x & 0xffff_ffff_ffff_ffff)
|
80
|
+
end
|
81
|
+
|
82
|
+
def scan_uint128
|
83
|
+
(scan_uint64 << 64) + scan_uint64
|
84
|
+
end
|
85
|
+
|
86
|
+
def add_time(time)
|
87
|
+
add_uint64((time.to_f * 1000).to_i)
|
88
|
+
end
|
89
|
+
|
90
|
+
def scan_time
|
91
|
+
Time.at(scan_uint64.to_f / 1000)
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_ip(ipaddr)
|
95
|
+
if ipaddr.ipv4?
|
96
|
+
add_uint8(4)
|
97
|
+
add_uint32(ipaddr.to_i)
|
98
|
+
elsif ipaddr.ipv6?
|
99
|
+
add_uint8(6)
|
100
|
+
add_uint128(ipaddr.to_i)
|
101
|
+
else
|
102
|
+
raise InvalidBuffer, "Unsupported IP address: #{ipaddr.to_s}"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def scan_ip
|
107
|
+
type = scan_uint8
|
108
|
+
case type
|
109
|
+
when 4
|
110
|
+
IPAddr.new_ntoh scan(4)
|
111
|
+
when 6
|
112
|
+
IPAddr.new_ntoh scan(16)
|
113
|
+
else
|
114
|
+
raise InvalidBuffer, "Unsupported IP address family: #{type}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def add_varbytes(bytes)
|
119
|
+
size = bytes.size
|
120
|
+
raise InvalidBuffer, "String too long: #{size}" if size > MAX_STRING_SIZE
|
121
|
+
add_uint32 size
|
122
|
+
self << bytes
|
123
|
+
end
|
124
|
+
|
125
|
+
def scan_varbytes
|
126
|
+
size = scan_uint32
|
127
|
+
raise InvalidBuffer, "String too long: #{size}" if size > MAX_STRING_SIZE
|
128
|
+
scan(size)
|
129
|
+
end
|
130
|
+
|
131
|
+
def add_string(string)
|
132
|
+
if string.respond_to?(:encode)
|
133
|
+
add_varbytes string.encode('BINARY')
|
134
|
+
else
|
135
|
+
add_varbytes string
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def scan_string
|
140
|
+
string = scan_varbytes
|
141
|
+
if string.respond_to?(:encode)
|
142
|
+
string.encode('UTF-8')
|
143
|
+
else
|
144
|
+
string
|
145
|
+
end
|
146
|
+
rescue EncodingError => e
|
147
|
+
raise InvalidBuffer, e
|
148
|
+
end
|
149
|
+
|
150
|
+
def add_buffer(buffer)
|
151
|
+
add_varbytes buffer.raw
|
152
|
+
end
|
153
|
+
|
154
|
+
def scan_buffer
|
155
|
+
Octokey::Buffer.from_raw scan_varbytes
|
156
|
+
end
|
157
|
+
|
158
|
+
def add_mpint(x)
|
159
|
+
raise InvalidBuffer, "Got negative mpint" if x < 0
|
160
|
+
bytes = OpenSSL::BN.new(x.to_s, 10).to_s(2)
|
161
|
+
bytes = "\x00" + bytes if bytes.bytes.first >= 0x80
|
162
|
+
add_varbytes(bytes)
|
163
|
+
end
|
164
|
+
|
165
|
+
def scan_mpint
|
166
|
+
bytes = scan_varbytes
|
167
|
+
|
168
|
+
if bytes.bytes.first >= 0x80
|
169
|
+
raise InvalidBuffer, "Got negative mpint"
|
170
|
+
end
|
171
|
+
|
172
|
+
OpenSSL::BN.new(bytes, 2)
|
173
|
+
end
|
174
|
+
|
175
|
+
def inspect
|
176
|
+
"#<Octokey::Buffer @buffer=#{to_s.inspect}>"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
data/lib/octokey.rb
ADDED
@@ -0,0 +1,366 @@
|
|
1
|
+
require File.expand_path('octokey/buffer', File.dirname(__FILE__))
|
2
|
+
require 'ipaddr'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
class Octokey
|
7
|
+
|
8
|
+
CHALLENGE_VERSION = 2
|
9
|
+
SERVICE_NAME = "octokey-auth"
|
10
|
+
AUTH_METHOD = "publickey"
|
11
|
+
SIGNING_ALGORITHM = "ssh-rsa"
|
12
|
+
DIGEST_ALGORITHM = "SHA1"
|
13
|
+
SSH_RSA_MINIMUM_MODULUS_SIZE = 768
|
14
|
+
|
15
|
+
class InvalidRequest < StandardError; end
|
16
|
+
class InvalidBuffer < InvalidRequest; end
|
17
|
+
|
18
|
+
# Set the hmac_secret for Octokey.
|
19
|
+
#
|
20
|
+
# This is used to sign challenges to prove that they were issued by us.
|
21
|
+
#
|
22
|
+
# You can generate a suitable token to use as an hmac_secret from the
|
23
|
+
# command line:
|
24
|
+
#
|
25
|
+
# $ head -c 48 /dev/urandom | base64
|
26
|
+
#
|
27
|
+
# @param [String] hmac_secret
|
28
|
+
def self.hmac_secret=(hmac_secret)
|
29
|
+
@hmac_secret = hmac_secret
|
30
|
+
@hmac_secret_fingerprint = nil
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get a challenge for signing in.
|
34
|
+
#
|
35
|
+
# The client will include this challenge in their octokey auth request when
|
36
|
+
# they log in. It hopefully provides some security against replay attacks by
|
37
|
+
# ensuring that if a signed auth-request is stolen, it is only valid in a
|
38
|
+
# limited set of circumstances.
|
39
|
+
#
|
40
|
+
# Please be careful when obtaining a client IP address that you aren't getting
|
41
|
+
# the IP address of an upstream proxy, and that you aren't trusting X-Forwarded-For
|
42
|
+
# headers that you shouldn't be.
|
43
|
+
#
|
44
|
+
# @option opts [String] :client_ip The IP address of the current client.
|
45
|
+
# @option opts [Time] :time (Time.now)
|
46
|
+
#
|
47
|
+
# @return [String]
|
48
|
+
def self.new_challenge(opts = {})
|
49
|
+
client_ip = opts[:client_ip] or raise ArgumentError, "No :client_ip given to new_challenge_for"
|
50
|
+
time = opts[:time] || Time.now
|
51
|
+
|
52
|
+
client_ip = IPAddr.new(client_ip.to_s)
|
53
|
+
buffer = Octokey::Buffer.new
|
54
|
+
|
55
|
+
buffer.add_uint8 Octokey::CHALLENGE_VERSION
|
56
|
+
buffer.add_uint8 Octokey.hmac_secret_fingerprint
|
57
|
+
buffer.add_time time
|
58
|
+
buffer.add_ip client_ip
|
59
|
+
buffer.add_varbytes SecureRandom.random_bytes(32)
|
60
|
+
buffer.add_varbytes OpenSSL::HMAC.digest("sha1", Octokey.hmac_secret, buffer.raw)
|
61
|
+
|
62
|
+
buffer.to_s
|
63
|
+
end
|
64
|
+
|
65
|
+
# Attempt to login with the given auth_request.
|
66
|
+
#
|
67
|
+
# @param [String] auth_request The string sent by the Octokey client.
|
68
|
+
# @option opts [String] :client_ip The IP address of the client (see {.new_challenge)}
|
69
|
+
# @option opts [Array<String>] :valid_hostnames The list of hostnames which clients may
|
70
|
+
# log in from.
|
71
|
+
# @option opts [Time] :time (Time.now)
|
72
|
+
#
|
73
|
+
# @yield [String] username The block should (when given a username) return a list of
|
74
|
+
# public keys that are associated with that users account.
|
75
|
+
#
|
76
|
+
# NOTE: Do not assume that the username passed to the block
|
77
|
+
# is logged in. The block is necessarily called before we know
|
78
|
+
# this.
|
79
|
+
#
|
80
|
+
# @return [String] username The user who successfully authenticated.
|
81
|
+
# @raise [InvalidRequest] If the login failed for some reason.
|
82
|
+
def self.login(auth_request, opts = {}, &block)
|
83
|
+
client_ip = opts[:client_ip] or raise ArgumentError, "No :client_ip given to login"
|
84
|
+
hostnames = opts[:valid_hostnames] or raise ArgumentError, "No :valid_hostnames given to login"
|
85
|
+
time = opts[:time] || Time.now
|
86
|
+
raise ArgumentError, "No public key lookup block given to login" unless block_given?
|
87
|
+
|
88
|
+
buffer = Octokey::Buffer.new(auth_request)
|
89
|
+
|
90
|
+
challenge = buffer.scan_string rescue ""
|
91
|
+
request_url = buffer.scan_string
|
92
|
+
username = buffer.scan_string
|
93
|
+
service_name = buffer.scan_string
|
94
|
+
auth_method = buffer.scan_string
|
95
|
+
signing_alg = buffer.scan_string
|
96
|
+
public_key_b = buffer.scan_buffer
|
97
|
+
signature_b = buffer.scan_buffer
|
98
|
+
|
99
|
+
valid_public_keys = block.call(username)
|
100
|
+
valid_public_keys.map!{ |public_key| format_public_key(unformat_public_key(public_key)) }
|
101
|
+
|
102
|
+
public_key, errors = decode_public_key(public_key_b, "ssh-rsa")
|
103
|
+
signature, sig_errors = decode_signature(signature_b, signing_alg)
|
104
|
+
|
105
|
+
errors += sig_errors
|
106
|
+
errors += validate_challenge(challenge, client_ip, time)
|
107
|
+
|
108
|
+
hostname = URI.parse(request_url).host
|
109
|
+
|
110
|
+
to_verify = Octokey::Buffer.new
|
111
|
+
to_verify.add_string challenge
|
112
|
+
to_verify.add_string request_url
|
113
|
+
to_verify.add_string username
|
114
|
+
to_verify.add_string service_name
|
115
|
+
to_verify.add_string auth_method
|
116
|
+
to_verify.add_string signing_alg
|
117
|
+
to_verify.add_buffer public_key_b
|
118
|
+
|
119
|
+
expected_digest = OpenSSL::Digest::SHA1.digest(to_verify.raw)
|
120
|
+
raw_asn1 = public_key.public_decrypt(signature)
|
121
|
+
|
122
|
+
errors += validate_digest(raw_asn1, expected_digest)
|
123
|
+
|
124
|
+
unless buffer.empty?
|
125
|
+
errors << "Request contained trailing bytes"
|
126
|
+
end
|
127
|
+
|
128
|
+
unless hostnames.include?(hostname)
|
129
|
+
errors << "Request was for unknown hostname: #{hostname.inspect}"
|
130
|
+
end
|
131
|
+
|
132
|
+
unless service_name == SERVICE_NAME
|
133
|
+
errors << "Incorrect service name: Got #{service_name.inspect}, expected: #{SERVICE_NAME.inspect}"
|
134
|
+
end
|
135
|
+
|
136
|
+
unless auth_method == AUTH_METHOD
|
137
|
+
errors << "Incorrect auth type: Got #{auth_method.inspect}, expected: #{AUTH_TYPE.inspect}"
|
138
|
+
end
|
139
|
+
|
140
|
+
unless signing_alg == SIGNING_ALGORITHM
|
141
|
+
errors << "Incorrect signing algorithm: Got #{signing_alg.inspect}, expected: #{SIGNING_ALGORITHM.inspect}"
|
142
|
+
end
|
143
|
+
|
144
|
+
unless valid_public_keys.include?(format_public_key(public_key))
|
145
|
+
errors << "Got unknown public key for #{username.inspect}: #{format_public_key(public_key).inspect}"
|
146
|
+
end
|
147
|
+
|
148
|
+
unless errors.empty?
|
149
|
+
raise InvalidRequest.new("Octokey request failed: #{errors.join(". ")}.")
|
150
|
+
end
|
151
|
+
|
152
|
+
username
|
153
|
+
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def self.hmac_secret
|
158
|
+
@hmac_secret or raise "No Octokey.hmac_secret set."
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.hmac_secret_fingerprint
|
162
|
+
@hmac_secret_fingerprint ||= OpenSSL::Digest::SHA1.digest(hmac_secret).bytes.first
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.validate_challenge(challenge, expected_ip, expected_time = Time.now)
|
166
|
+
buffer = Octokey::Buffer.new challenge
|
167
|
+
errors = []
|
168
|
+
|
169
|
+
version = buffer.scan_uint8
|
170
|
+
if version != Octokey::CHALLENGE_VERSION
|
171
|
+
errors << "Challenge Version number is incorrect: Got #{version} expected #{Octokey::CHALLENGE_VERSION}"
|
172
|
+
end
|
173
|
+
|
174
|
+
fingerprint = buffer.scan_uint8
|
175
|
+
if fingerprint != hmac_secret_fingerprint
|
176
|
+
errors << "Challenge HMAC was signed with a different secret: Got #{fingerprint.inspect}, expected: #{hmac_secret_fingerprint.inspect}"
|
177
|
+
end
|
178
|
+
|
179
|
+
time = buffer.scan_time
|
180
|
+
puts time.inspect
|
181
|
+
if expected_time - time > 5 * 60
|
182
|
+
errors << "Challenge Timestamp is too dated: Got #{time.to_i}, expected > #{expected_time.to_i - 5 * 60}"
|
183
|
+
end
|
184
|
+
|
185
|
+
if time - expected_time > 5
|
186
|
+
errors << "Challenge Timestamp is too new: Got #{time.to_i}, expected < #{expected_time.to_i + 5}"
|
187
|
+
end
|
188
|
+
|
189
|
+
client_ip = buffer.scan_ip
|
190
|
+
if client_ip != expected_ip
|
191
|
+
errors << "Client IP address mismatch: Got #{client_ip}, expected: #{expected_ip}"
|
192
|
+
end
|
193
|
+
|
194
|
+
random = buffer.scan_varbytes
|
195
|
+
hmac = buffer.scan_varbytes
|
196
|
+
|
197
|
+
if !buffer.empty?
|
198
|
+
errors << "Challenge contained trailing bytes"
|
199
|
+
end
|
200
|
+
|
201
|
+
rehash = Octokey::Buffer.new
|
202
|
+
rehash.add_uint8 version
|
203
|
+
rehash.add_uint8 fingerprint
|
204
|
+
rehash.add_time time
|
205
|
+
rehash.add_ip client_ip
|
206
|
+
rehash.add_varbytes random
|
207
|
+
expected_hmac = OpenSSL::HMAC.digest("sha1", Octokey.hmac_secret, rehash.raw)
|
208
|
+
|
209
|
+
if hmac != expected_hmac
|
210
|
+
errors << "Challenge HMAC was incorrect: Got #{hmac.inspect}, expected: #{expected_hmac.inspect}"
|
211
|
+
end
|
212
|
+
|
213
|
+
errors
|
214
|
+
rescue InvalidBuffer => e
|
215
|
+
errors + [e.message]
|
216
|
+
end
|
217
|
+
|
218
|
+
def self.sign_challenge(challenge, username, request_url, private_key)
|
219
|
+
to_sign = Octokey::Buffer.new
|
220
|
+
to_sign.add_string challenge
|
221
|
+
to_sign.add_string request_url
|
222
|
+
to_sign.add_string username
|
223
|
+
to_sign.add_string SERVICE_NAME
|
224
|
+
to_sign.add_string AUTH_METHOD
|
225
|
+
to_sign.add_string SIGNING_ALGORITHM
|
226
|
+
to_sign.add_buffer encode_public_key(private_key)
|
227
|
+
|
228
|
+
digest = OpenSSL::Digest::SHA1.digest(to_sign.raw)
|
229
|
+
sigblob = private_key.private_encrypt(digest_into_asn1(digest))
|
230
|
+
|
231
|
+
sig_buf = Octokey::Buffer.new
|
232
|
+
sig_buf.add_string SIGNING_ALGORITHM
|
233
|
+
sig_buf.add_string sigblob
|
234
|
+
|
235
|
+
to_sign.add_buffer(sig_buf)
|
236
|
+
|
237
|
+
to_sign.to_s
|
238
|
+
end
|
239
|
+
|
240
|
+
# TODO: replace this by a call to OpenSSL
|
241
|
+
def self.digest_into_asn1(digest)
|
242
|
+
raise "not a valid digest" unless digest.size == 20
|
243
|
+
"0!0\t\x06\x05+\x0E\x03\x02\x1A\x05\x00\x04\x14#{digest}"
|
244
|
+
end
|
245
|
+
|
246
|
+
# TODO: replace this by a call to OpenSSL
|
247
|
+
#
|
248
|
+
# The ASN1 structure should look like this:
|
249
|
+
#
|
250
|
+
# [
|
251
|
+
# ["SHA-1", nil],
|
252
|
+
# "digestdigestdigest.."
|
253
|
+
# ]
|
254
|
+
def self.validate_digest(raw_asn1, expected_digest)
|
255
|
+
asn1 = OpenSSL::ASN1.decode(raw_asn1)
|
256
|
+
errors = []
|
257
|
+
|
258
|
+
unless asn1.tag == 16 && asn1.value.size == 2
|
259
|
+
return ["Invalid asn1 was signed"]
|
260
|
+
end
|
261
|
+
|
262
|
+
algorithm_asn1 = asn1.value[0]
|
263
|
+
digest_asn1 = asn1.value[1]
|
264
|
+
|
265
|
+
unless digest_asn1.tag == 4
|
266
|
+
return ["Invalid digest_asn1 was signed"]
|
267
|
+
end
|
268
|
+
|
269
|
+
unless algorithm_asn1.tag == 16 && algorithm_asn1.value.size <= 2
|
270
|
+
return ["Invalid algorithm_asn1 was signed"]
|
271
|
+
end
|
272
|
+
|
273
|
+
unless algorithm_asn1.value[0].value == DIGEST_ALGORITHM
|
274
|
+
errors << "Incorrect digest algorithm: Got #{algorithm_asn1.value[0].value}, expected: #{DIGEST_ALGORITHM.inspect}"
|
275
|
+
end
|
276
|
+
|
277
|
+
unless algorithm_asn1.value.size == 1 || algorithm_asn1.value[1].is_a?(OpenSSL::ASN1::Null)
|
278
|
+
errors << "Invalid parameters passed to SHA1 digest, expected NULL."
|
279
|
+
end
|
280
|
+
|
281
|
+
unless digest_asn1.value == expected_digest
|
282
|
+
errors << "Incorrect message digest"
|
283
|
+
end
|
284
|
+
|
285
|
+
return errors
|
286
|
+
end
|
287
|
+
|
288
|
+
def self.decode_signature(buffer, expected_alg)
|
289
|
+
buffer = buffer.dup
|
290
|
+
|
291
|
+
signing_alg = buffer.scan_string
|
292
|
+
signature = buffer.scan_varbytes
|
293
|
+
|
294
|
+
errors = []
|
295
|
+
|
296
|
+
unless buffer.empty?
|
297
|
+
errors << "Signature contained trailing bytes"
|
298
|
+
end
|
299
|
+
|
300
|
+
unless signing_alg == expected_alg
|
301
|
+
errors << "Signature algorithm mismatch: Got #{signing_alg.inspect}, expected: #{expected_alg.inspect}"
|
302
|
+
end
|
303
|
+
|
304
|
+
[signature, errors]
|
305
|
+
rescue InvalidBuffer => e
|
306
|
+
[nil, e.message]
|
307
|
+
end
|
308
|
+
|
309
|
+
def self.format_public_key(public_key)
|
310
|
+
"ssh-rsa #{encode_public_key(public_key).to_s}"
|
311
|
+
end
|
312
|
+
|
313
|
+
def self.unformat_public_key(public_key)
|
314
|
+
if public_key =~ /\A(ssh-rsa)\s+(.*)\z/
|
315
|
+
key, errors = decode_public_key(Octokey::Buffer.new($2), $1)
|
316
|
+
raise "Invalid public key: #{errors.join(". ")}." unless errors.empty?
|
317
|
+
|
318
|
+
key
|
319
|
+
else
|
320
|
+
raise "Invalid public key: Got #{public_key.inspect}, expected \"ssh-rsa AAAAf...\""
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
def self.encode_public_key(public_key)
|
325
|
+
raise "not an RSA key: #{public_key}" unless OpenSSL::PKey::RSA === public_key
|
326
|
+
buffer = Octokey::Buffer.new
|
327
|
+
buffer.add_string "ssh-rsa"
|
328
|
+
buffer.add_mpint public_key.e
|
329
|
+
buffer.add_mpint public_key.n
|
330
|
+
buffer
|
331
|
+
end
|
332
|
+
|
333
|
+
def self.decode_public_key(buffer, expected_type)
|
334
|
+
buffer = buffer.dup
|
335
|
+
|
336
|
+
key_type = buffer.scan_string
|
337
|
+
e = buffer.scan_mpint
|
338
|
+
n = buffer.scan_mpint
|
339
|
+
|
340
|
+
errors = []
|
341
|
+
|
342
|
+
unless buffer.empty?
|
343
|
+
errors << "Public key contained trailing bytes"
|
344
|
+
end
|
345
|
+
|
346
|
+
unless key_type == expected_type
|
347
|
+
errors << "Got unknown public key type: Got #{key_type.inspect}, expected: #{expected_type.inspect}"
|
348
|
+
end
|
349
|
+
|
350
|
+
unless n.num_bits > SSH_RSA_MINIMUM_MODULUS_SIZE
|
351
|
+
errors << "RSA modulus too small: #{n.num_bits.inspect} < #{SSH_RSA_MINIMUM_MODULUS_SIZE.inspect}"
|
352
|
+
end
|
353
|
+
|
354
|
+
# TODO: verify size of modulus and exponent
|
355
|
+
|
356
|
+
if errors == []
|
357
|
+
key = OpenSSL::PKey::RSA.new
|
358
|
+
key.e = e
|
359
|
+
key.n = n
|
360
|
+
end
|
361
|
+
|
362
|
+
[key, errors]
|
363
|
+
rescue InvalidBuffer => e
|
364
|
+
[nil, e.message]
|
365
|
+
end
|
366
|
+
end
|
metadata
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: octokey
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.pre.1
|
5
|
+
prerelease: 4
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Conrad Irwin
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-06-25 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Allows you to use secure authentication mechanisms in plcae of passwords
|
15
|
+
email: conrad.irwin@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/octokey.rb
|
21
|
+
- lib/octokey/buffer.rb
|
22
|
+
homepage: https://github.com/octokey/octokey-gem
|
23
|
+
licenses: []
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>'
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 1.3.1
|
40
|
+
requirements: []
|
41
|
+
rubyforge_project:
|
42
|
+
rubygems_version: 1.8.24
|
43
|
+
signing_key:
|
44
|
+
specification_version: 3
|
45
|
+
summary: Public key authentication for the web!
|
46
|
+
test_files: []
|