octokey 0.1.pre.2 → 0.1.pre.3

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.rb CHANGED
@@ -1,390 +1,149 @@
1
- require File.expand_path('octokey/buffer', File.dirname(__FILE__))
2
- require 'ipaddr'
1
+ require 'openssl'
3
2
  require 'securerandom'
3
+ require 'ipaddr'
4
4
  require 'uri'
5
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
6
+ require File.expand_path('octokey/buffer', File.dirname(__FILE__))
7
+ require File.expand_path('octokey/config', File.dirname(__FILE__))
8
+ require File.expand_path('octokey/challenge', File.dirname(__FILE__))
9
+ require File.expand_path('octokey/public_key', File.dirname(__FILE__))
10
+ require File.expand_path('octokey/auth_request', File.dirname(__FILE__))
14
11
 
12
+ # Octokey is a private key based login mechanism for websites inspired
13
+ # heavily by, and borrowing algorithms from, OpenSSH.
14
+ class Octokey
15
+ # Raised when you try and access details of an invalid octokey request.
16
+ # If you always check .can_log_in? or .can_sign_up? first, you should not
17
+ # see this exception.
15
18
  class InvalidRequest < StandardError; end
19
+
20
+ # Raised if an Octokey::Buffer is invalid. This is usually caught by Octokey
21
+ # so you will only need to catch it if you are parsing buffers yourself.
16
22
  class InvalidBuffer < InvalidRequest; end
17
23
 
18
- # Set the hmac_secret for Octokey.
24
+ # Create a new challenge.
19
25
  #
20
- # This is used to sign challenges to prove that they were issued by us.
26
+ # Once created, the challenge should be sent to the client for it to sign.
21
27
  #
22
- # You can generate a suitable token to use as an hmac_secret from the
23
- # command line:
28
+ # The client_ip address is included in the outgoing challenge to verify that
29
+ # incoming login requests came from the same client as requested the challenge.
24
30
  #
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
+ # @param [Hash] opts
32
+ # @option opts [String,IPAddr] client_ip The IP address of the client.
33
+ # @return [String] The Base64 encoded challenge.
34
+ def self.new_challenge(opts)
35
+ Octokey::Challenge.generate(opts).to_s
31
36
  end
32
37
 
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.
38
+ # Sign a challenge.
39
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.
40
+ # If you're acting as an Octokey client, then you use this function to turn
41
+ # a challenge that you've been issued into an auth_request to send back to the
42
+ # server.
43
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
44
+ # @param [String] challenge The base64-encoded challenge issued by the server.
45
+ # @param [Hash] opts
46
+ # @option opts [String] :username Which username would you like to log in as.
47
+ # @option opts [String] :request_url Which page would you like to log in to.
48
+ # @option opts [OpenSSL::PKey::RSA] :private_key The private key with which to sign the challenge.
49
+ # @return [String] The Base64 encoded auth_request
50
+ def self.sign_challenge(challenge, opts)
51
+ Octokey::AuthRequest.generate({:challenge => challenge}.merge(opts)).to_s
63
52
  end
64
53
 
65
- # Attempt to login with the given auth_request.
54
+ # Handle a new Octokey request.
66
55
  #
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)
56
+ # The options passed in to this method will be passed in as the second parameter to
57
+ # all the configuration blocks.
72
58
  #
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
- raise ArgumentError, "No public key lookup block given to login" unless block_given?
84
-
85
- username, public_key = signup(auth_request, opts)
86
- valid_public_keys = block.call(username)
87
- valid_public_keys.map!{ |public_key| format_public_key(unformat_public_key(public_key)) }
88
-
89
- unless valid_public_keys.include? public_key
90
- raise InvalidRequest, "Got unknown public key for #{username.inspect}: #{format_public_key(public_key).inspect}"
91
- end
92
-
93
- username
59
+ # @param [String] auth_request The Base64 encoded auth request from the client.
60
+ # @param [Hash] opts
61
+ # @option opts [String] :username The username that the user wishes to log in as.
62
+ # @option opts [String,IPAddr] :client_ip The ip address of the client.
63
+ def initialize(auth_request, opts)
64
+ raise ArgumentError, "no :username given" unless opts[:username]
65
+ raise ArgumentError, "no :client_ip given" unless opts[:client_ip]
66
+ @opts = opts.dup
67
+ @auth_request = Octokey::Config.get_auth_request(auth_request, opts)
94
68
  end
95
69
 
96
- # Validate a signup request.
97
- #
98
- # @param [String] auth_request The string sent by the Octokey client.
99
- # @option opts [String] :client_ip The IP address of the client (see {.new_challenge)}
100
- # @option opts [Array<String>] :valid_hostnames The list of hostnames which clients may
101
- # log in from.
102
- # @option opts [Time] :time (Time.now)
103
- #
104
- # @yield [String] username The block should (when given a username) return a list of
105
- # public keys that are associated with that users account.
70
+ # Should the user be allowed to log in?
106
71
  #
107
- # NOTE: Do not assume that the username passed to the block
108
- # is logged in. The block is necessarily called before we know
109
- # this.
72
+ # If this method returns true then you can assume that the user with :username
73
+ # is actually the user who's trying to log in.
110
74
  #
111
- # @return [String] username The username they tried to sign up with.
112
- # @return [String] public_key Their public key
113
- # @raise [InvalidRequest] If the login failed for some reason.
114
- def self.signup(auth_request, opts = {})
115
- client_ip = opts[:client_ip] or raise ArgumentError, "No :client_ip given to login"
116
- hostnames = opts[:valid_hostnames] or raise ArgumentError, "No :valid_hostnames given to login"
117
- time = opts[:time] || Time.now
118
-
119
- buffer = Octokey::Buffer.new(auth_request)
120
-
121
- challenge = buffer.scan_string rescue ""
122
- request_url = buffer.scan_string
123
- username = buffer.scan_string
124
- service_name = buffer.scan_string
125
- auth_method = buffer.scan_string
126
- signing_alg = buffer.scan_string
127
- public_key_b = buffer.scan_buffer
128
- signature_b = buffer.scan_buffer
129
-
130
- public_key, errors = decode_public_key(public_key_b, "ssh-rsa")
131
- signature, sig_errors = decode_signature(signature_b, signing_alg)
132
-
133
- errors += sig_errors
134
- errors += validate_challenge(challenge, client_ip, time)
135
-
136
- hostname = URI.parse(request_url).host
137
-
138
- to_verify = Octokey::Buffer.new
139
- to_verify.add_string challenge
140
- to_verify.add_string request_url
141
- to_verify.add_string username
142
- to_verify.add_string service_name
143
- to_verify.add_string auth_method
144
- to_verify.add_string signing_alg
145
- to_verify.add_buffer public_key_b
146
-
147
- expected_digest = OpenSSL::Digest::SHA1.digest(to_verify.raw)
148
- raw_asn1 = public_key.public_decrypt(signature)
149
-
150
- errors += validate_digest(raw_asn1, expected_digest)
151
-
152
- unless buffer.empty?
153
- errors << "Request contained trailing bytes"
154
- end
155
-
156
- unless hostnames.include?(hostname)
157
- errors << "Request was for unknown hostname: #{hostname.inspect}"
158
- end
159
-
160
- unless service_name == SERVICE_NAME
161
- errors << "Incorrect service name: Got #{service_name.inspect}, expected: #{SERVICE_NAME.inspect}"
162
- end
163
-
164
- unless auth_method == AUTH_METHOD
165
- errors << "Incorrect auth type: Got #{auth_method.inspect}, expected: #{AUTH_TYPE.inspect}"
166
- end
167
-
168
- unless signing_alg == SIGNING_ALGORITHM
169
- errors << "Incorrect signing algorithm: Got #{signing_alg.inspect}, expected: #{SIGNING_ALGORITHM.inspect}"
170
- end
171
-
172
- unless errors.empty?
173
- raise InvalidRequest.new("Octokey request failed: #{errors.join(". ")}.")
174
- end
175
-
176
- [username, format_public_key(public_key)]
177
- end
178
-
179
- private
180
-
181
- def self.hmac_secret
182
- @hmac_secret or raise "No Octokey.hmac_secret set."
183
- end
184
-
185
- def self.hmac_secret_fingerprint
186
- @hmac_secret_fingerprint ||= OpenSSL::Digest::SHA1.digest(hmac_secret).bytes.first
75
+ # @return [Boolean]
76
+ def can_log_in?
77
+ valid_auth_request? && valid_public_key?
187
78
  end
188
79
 
189
- def self.validate_challenge(challenge, expected_ip, expected_time = Time.now)
190
- buffer = Octokey::Buffer.new challenge
191
- errors = []
192
-
193
- version = buffer.scan_uint8
194
- if version != Octokey::CHALLENGE_VERSION
195
- errors << "Challenge Version number is incorrect: Got #{version} expected #{Octokey::CHALLENGE_VERSION}"
196
- end
197
-
198
- fingerprint = buffer.scan_uint8
199
- if fingerprint != hmac_secret_fingerprint
200
- errors << "Challenge HMAC was signed with a different secret: Got #{fingerprint.inspect}, expected: #{hmac_secret_fingerprint.inspect}"
201
- end
202
-
203
- time = buffer.scan_time
204
- puts time.inspect
205
- if expected_time - time > 5 * 60
206
- errors << "Challenge Timestamp is too dated: Got #{time.to_i}, expected > #{expected_time.to_i - 5 * 60}"
207
- end
208
-
209
- if time - expected_time > 5
210
- errors << "Challenge Timestamp is too new: Got #{time.to_i}, expected < #{expected_time.to_i + 5}"
211
- end
212
-
213
- client_ip = buffer.scan_ip
214
- if client_ip != expected_ip
215
- errors << "Client IP address mismatch: Got #{client_ip}, expected: #{expected_ip}"
216
- end
217
-
218
- random = buffer.scan_varbytes
219
- hmac = buffer.scan_varbytes
220
-
221
- if !buffer.empty?
222
- errors << "Challenge contained trailing bytes"
223
- end
224
-
225
- rehash = Octokey::Buffer.new
226
- rehash.add_uint8 version
227
- rehash.add_uint8 fingerprint
228
- rehash.add_time time
229
- rehash.add_ip client_ip
230
- rehash.add_varbytes random
231
- expected_hmac = OpenSSL::HMAC.digest("sha1", Octokey.hmac_secret, rehash.raw)
232
-
233
- if hmac != expected_hmac
234
- errors << "Challenge HMAC was incorrect: Got #{hmac.inspect}, expected: #{expected_hmac.inspect}"
235
- end
236
-
237
- errors
238
- rescue InvalidBuffer => e
239
- errors + [e.message]
240
- end
241
-
242
- def self.sign_challenge(challenge, username, request_url, private_key)
243
- to_sign = Octokey::Buffer.new
244
- to_sign.add_string challenge
245
- to_sign.add_string request_url
246
- to_sign.add_string username
247
- to_sign.add_string SERVICE_NAME
248
- to_sign.add_string AUTH_METHOD
249
- to_sign.add_string SIGNING_ALGORITHM
250
- to_sign.add_buffer encode_public_key(private_key)
251
-
252
- digest = OpenSSL::Digest::SHA1.digest(to_sign.raw)
253
- sigblob = private_key.private_encrypt(digest_into_asn1(digest))
254
-
255
- sig_buf = Octokey::Buffer.new
256
- sig_buf.add_string SIGNING_ALGORITHM
257
- sig_buf.add_string sigblob
258
-
259
- to_sign.add_buffer(sig_buf)
260
-
261
- to_sign.to_s
262
- end
263
-
264
- # TODO: replace this by a call to OpenSSL
265
- def self.digest_into_asn1(digest)
266
- raise "not a valid digest" unless digest.size == 20
267
- "0!0\t\x06\x05+\x0E\x03\x02\x1A\x05\x00\x04\x14#{digest}"
80
+ # Should the user be allowed to sign up?
81
+ #
82
+ # If this method returns true then you can store the public_key against the
83
+ # user's username. Future logins by that user will use that to verify that the user
84
+ # trying to log in has access to the private key that corresponds to this public key.
85
+ #
86
+ # @return [Boolean]
87
+ def can_sign_up?
88
+ valid_auth_request?
268
89
  end
269
90
 
270
- # TODO: replace this by a call to OpenSSL
91
+ # Was the failure to log in or sign up transient?
271
92
  #
272
- # The ASN1 structure should look like this:
93
+ # This will return true if the client may be able to log in simply by requesting
94
+ # a new challenge from the server and retrying the auth_request.
273
95
  #
274
- # [
275
- # ["SHA-1", nil],
276
- # "digestdigestdigest.."
277
- # ]
278
- def self.validate_digest(raw_asn1, expected_digest)
279
- asn1 = OpenSSL::ASN1.decode(raw_asn1)
280
- errors = []
281
-
282
- unless asn1.tag == 16 && asn1.value.size == 2
283
- return ["Invalid asn1 was signed"]
284
- end
285
-
286
- algorithm_asn1 = asn1.value[0]
287
- digest_asn1 = asn1.value[1]
288
-
289
- unless digest_asn1.tag == 4
290
- return ["Invalid digest_asn1 was signed"]
291
- end
292
-
293
- unless algorithm_asn1.tag == 16 && algorithm_asn1.value.size <= 2
294
- return ["Invalid algorithm_asn1 was signed"]
295
- end
296
-
297
- unless algorithm_asn1.value[0].value == DIGEST_ALGORITHM
298
- errors << "Incorrect digest algorithm: Got #{algorithm_asn1.value[0].value}, expected: #{DIGEST_ALGORITHM.inspect}"
299
- end
300
-
301
- unless algorithm_asn1.value.size == 1 || algorithm_asn1.value[1].is_a?(OpenSSL::ASN1::Null)
302
- errors << "Invalid parameters passed to SHA1 digest, expected NULL."
303
- end
304
-
305
- unless digest_asn1.value == expected_digest
306
- errors << "Incorrect message digest"
307
- end
308
-
309
- return errors
96
+ # @return [Boolean]
97
+ def should_retry?
98
+ !valid_auth_request? && auth_request.valid_ignoring_challenge?(opts)
310
99
  end
311
100
 
312
- def self.decode_signature(buffer, expected_alg)
313
- buffer = buffer.dup
314
-
315
- signing_alg = buffer.scan_string
316
- signature = buffer.scan_varbytes
317
-
318
- errors = []
319
-
320
- unless buffer.empty?
321
- errors << "Signature contained trailing bytes"
322
- end
323
-
324
- unless signing_alg == expected_alg
325
- errors << "Signature algorithm mismatch: Got #{signing_alg.inspect}, expected: #{expected_alg.inspect}"
326
- end
327
-
328
- [signature, errors]
329
- rescue InvalidBuffer => e
330
- [nil, e.message]
101
+ # Get the username used for this request.
102
+ #
103
+ # You must validate that the username meets your requirements for a valid username,
104
+ # Octokey allows any username (for example the empty string, the string "\r\n"). You might
105
+ # want to enforce that the username is an email address, or contains only visible characters.
106
+ #
107
+ # @return [String] username
108
+ # @raise [InvalidRequest] if neither .can_sign_up? nor .can_log_in?
109
+ def username
110
+ raise InvalidRequest, "Tried to get username from invalid octokey" unless valid_auth_request?
111
+ auth_request.username
331
112
  end
332
113
 
333
- def self.format_public_key(public_key)
334
- "ssh-rsa #{encode_public_key(public_key).to_s}"
114
+ # Get the public_key used for this request.
115
+ #
116
+ # You will need this when handling a sign up request for the user in order to
117
+ # extract the public key needed to log in.
118
+ #
119
+ # The format of the returned public key is exactly the same as used by SSH in the
120
+ # ~/.authorized_keys file. That format can be parsed by {Octokey::PublicKey} if you
121
+ # need more information.
122
+ #
123
+ # @return [String]
124
+ # @raise [InvalidRequest] if neither .can_sign_up? nor .can_log_in?
125
+ def public_key
126
+ raise InvalidRequest, "Tried to get username from invalid octokey" unless valid_auth_request?
127
+ auth_request.public_key.to_s
335
128
  end
336
129
 
337
- def self.unformat_public_key(public_key)
338
- if public_key =~ /\A(ssh-rsa)\s+(.*)\z/
339
- key, errors = decode_public_key(Octokey::Buffer.new($2), $1)
340
- raise "Invalid public key: #{errors.join(". ")}." unless errors.empty?
341
-
342
- key
343
- else
344
- raise "Invalid public key: Got #{public_key.inspect}, expected \"ssh-rsa AAAAf...\""
345
- end
346
- end
130
+ private
131
+ attr_accessor :opts, :auth_request
347
132
 
348
- def self.encode_public_key(public_key)
349
- raise "not an RSA key: #{public_key}" unless OpenSSL::PKey::RSA === public_key
350
- buffer = Octokey::Buffer.new
351
- buffer.add_string "ssh-rsa"
352
- buffer.add_mpint public_key.e
353
- buffer.add_mpint public_key.n
354
- buffer
133
+ # Is the auth_request valid?
134
+ # @return [Boolean]
135
+ def valid_auth_request?
136
+ @valid ||= auth_request.valid?(opts)
355
137
  end
356
138
 
357
- def self.decode_public_key(buffer, expected_type)
358
- buffer = buffer.dup
359
-
360
- key_type = buffer.scan_string
361
- e = buffer.scan_mpint
362
- n = buffer.scan_mpint
363
-
364
- errors = []
365
-
366
- unless buffer.empty?
367
- errors << "Public key contained trailing bytes"
368
- end
369
-
370
- unless key_type == expected_type
371
- errors << "Got unknown public key type: Got #{key_type.inspect}, expected: #{expected_type.inspect}"
372
- end
373
-
374
- unless n.num_bits > SSH_RSA_MINIMUM_MODULUS_SIZE
375
- errors << "RSA modulus too small: #{n.num_bits.inspect} < #{SSH_RSA_MINIMUM_MODULUS_SIZE.inspect}"
139
+ # Is the public key used to sign the auth request one of those that belongs to the username?
140
+ # @return [Boolean]
141
+ def valid_public_key?
142
+ strings = Octokey::Config.get_public_keys(opts[:username], opts)
143
+ public_keys = strings.map{ |string| Octokey::PublicKey.from_string(string) }
144
+ unless public_keys.all?(&:valid?)
145
+ raise ArgumentError, "Invalid public key returned to Octokey for #{username}"
376
146
  end
377
-
378
- # TODO: verify size of modulus and exponent
379
-
380
- if errors == []
381
- key = OpenSSL::PKey::RSA.new
382
- key.e = e
383
- key.n = n
384
- end
385
-
386
- [key, errors]
387
- rescue InvalidBuffer => e
388
- [nil, e.message]
147
+ public_keys.include?(auth_request.public_key)
389
148
  end
390
149
  end
metadata CHANGED
@@ -1,46 +1,102 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: octokey
3
- version: !ruby/object:Gem::Version
4
- version: 0.1.pre.2
3
+ version: !ruby/object:Gem::Version
5
4
  prerelease: 4
5
+ version: 0.1.pre.3
6
6
  platform: ruby
7
- authors:
8
- - Conrad Irwin
7
+ authors:
8
+ - Conrad Irwin
9
9
  autorequire:
10
10
  bindir: bin
11
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
12
+
13
+ date: 2012-07-30 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ type: :development
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
27
+ name: active_support
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
30
+ none: false
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: "0"
35
+ type: :development
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
38
+ name: i18n
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: "0"
46
+ type: :development
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
49
+ name: simplecov
50
+ prerelease: false
51
+ requirement: &id004 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ type: :development
58
+ version_requirements: *id004
59
+ description: Allows you to use secure authentication mechanisms in place of passwords
15
60
  email: conrad.irwin@gmail.com
16
61
  executables: []
62
+
17
63
  extensions: []
64
+
18
65
  extra_rdoc_files: []
19
- files:
20
- - lib/octokey.rb
21
- - lib/octokey/buffer.rb
66
+
67
+ files:
68
+ - lib/octokey.rb
69
+ - lib/octokey/auth_request.rb
70
+ - lib/octokey/buffer.rb
71
+ - lib/octokey/challenge.rb
72
+ - lib/octokey/config.rb
73
+ - lib/octokey/public_key.rb
22
74
  homepage: https://github.com/octokey/octokey-gem
23
75
  licenses: []
76
+
24
77
  post_install_message:
25
78
  rdoc_options: []
26
- require_paths:
27
- - lib
28
- required_ruby_version: !ruby/object:Gem::Requirement
79
+
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
29
83
  none: false
30
- requirements:
31
- - - ! '>='
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- required_rubygems_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: "0"
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
89
  none: false
36
- requirements:
37
- - - ! '>'
38
- - !ruby/object:Gem::Version
39
- version: 1.3.1
90
+ requirements:
91
+ - - ">"
92
+ - !ruby/object:Gem::Version
93
+ version: 1.3.1
40
94
  requirements: []
95
+
41
96
  rubyforge_project:
42
97
  rubygems_version: 1.8.24
43
98
  signing_key:
44
99
  specification_version: 3
45
100
  summary: Public key authentication for the web!
46
101
  test_files: []
102
+