mach 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/README.md +4 -0
- data/Rakefile +6 -0
- data/examples/Gemfile +11 -0
- data/examples/Gemfile.lock +56 -0
- data/examples/Rakefile +26 -0
- data/examples/client.rb +36 -0
- data/examples/credential_server.ru +46 -0
- data/examples/validating_server.ru +27 -0
- data/lib/mach.rb +31 -0
- data/lib/mach/authorization_header.rb +46 -0
- data/lib/mach/configuration.rb +38 -0
- data/lib/mach/delta.rb +15 -0
- data/lib/mach/error/error.rb +10 -0
- data/lib/mach/faraday/request/hmac_authentication.rb +63 -0
- data/lib/mach/hmac.rb +27 -0
- data/lib/mach/nonce.rb +22 -0
- data/lib/mach/normalized_string.rb +17 -0
- data/lib/mach/persistence/delta_and_nonce_store.rb +27 -0
- data/lib/mach/persistence/in_memory_store.rb +48 -0
- data/lib/mach/persistence/redis_store.rb +40 -0
- data/lib/mach/rack/request_validator.rb +29 -0
- data/lib/mach/request.rb +51 -0
- data/lib/mach/signature.rb +30 -0
- data/lib/mach/timestamp.rb +9 -0
- data/lib/mach/validation/nonce_validator.rb +11 -0
- data/lib/mach/validation/request_validator.rb +23 -0
- data/lib/mach/validation/signature_validator.rb +24 -0
- data/lib/mach/validation/timestamp_validator.rb +21 -0
- data/lib/mach/version.rb +3 -0
- data/mach.gemspec +29 -0
- data/spec/normalized_string_spec.rb +34 -0
- data/spec/signature_spec.rb +55 -0
- data/spec/spec_helper.rb +14 -0
- metadata +200 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm --create ruby-1.9.3-p194@mach
|
data/Gemfile
ADDED
data/README.md
ADDED
data/Rakefile
ADDED
data/examples/Gemfile
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
GIT
|
2
|
+
remote: git@github.com:playup/credential_store.git
|
3
|
+
revision: 715e80adadc12bf5290dde096eaf3159a2d4a779
|
4
|
+
specs:
|
5
|
+
credential_store (0.0.5)
|
6
|
+
em-redis
|
7
|
+
em-synchrony
|
8
|
+
faraday
|
9
|
+
faraday_middleware
|
10
|
+
redis
|
11
|
+
|
12
|
+
PATH
|
13
|
+
remote: ../
|
14
|
+
specs:
|
15
|
+
mach (0.0.1)
|
16
|
+
faraday
|
17
|
+
multi_json
|
18
|
+
rack
|
19
|
+
redis
|
20
|
+
|
21
|
+
GEM
|
22
|
+
remote: http://rubygems.org/
|
23
|
+
specs:
|
24
|
+
em-redis (0.3.0)
|
25
|
+
eventmachine
|
26
|
+
em-synchrony (1.0.2)
|
27
|
+
eventmachine (>= 1.0.0.beta.1)
|
28
|
+
eventmachine (1.0.0.rc.4)
|
29
|
+
faraday (0.8.4)
|
30
|
+
multipart-post (~> 1.1)
|
31
|
+
faraday_middleware (0.8.8)
|
32
|
+
faraday (>= 0.7.4, < 0.9)
|
33
|
+
multi_json (1.3.6)
|
34
|
+
multipart-post (1.1.5)
|
35
|
+
rack (1.4.1)
|
36
|
+
rack-protection (1.2.0)
|
37
|
+
rack
|
38
|
+
rake (0.9.2.2)
|
39
|
+
redis (3.0.1)
|
40
|
+
sinatra (1.3.3)
|
41
|
+
rack (~> 1.3, >= 1.3.6)
|
42
|
+
rack-protection (~> 1.2)
|
43
|
+
tilt (~> 1.3, >= 1.3.3)
|
44
|
+
tilt (1.3.3)
|
45
|
+
|
46
|
+
PLATFORMS
|
47
|
+
ruby
|
48
|
+
|
49
|
+
DEPENDENCIES
|
50
|
+
credential_store!
|
51
|
+
mach!
|
52
|
+
multi_json
|
53
|
+
rack
|
54
|
+
rake
|
55
|
+
redis
|
56
|
+
sinatra
|
data/examples/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'rack/handler/webrick'
|
3
|
+
|
4
|
+
Dir['*.ru'].each do |server_config|
|
5
|
+
server_path = Pathname.new(server_config)
|
6
|
+
server_name = server_path.basename(server_path.extname)
|
7
|
+
namespace server_name.to_s.to_sym do
|
8
|
+
desc "Start the #{server_name}"
|
9
|
+
task :start do
|
10
|
+
Rack::Server.start(:config => server_config)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
task :client do
|
16
|
+
puts `ruby ./client.rb`
|
17
|
+
end
|
18
|
+
|
19
|
+
task :help do
|
20
|
+
puts 'Start the credential server first like so: rake credential_server:start'
|
21
|
+
puts 'Then start the validating server like so: rake validating_server:start'
|
22
|
+
puts 'And run the clien like: rake client'
|
23
|
+
end
|
24
|
+
|
25
|
+
task :default => :help
|
26
|
+
|
data/examples/client.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'mach'
|
4
|
+
require 'base64'
|
5
|
+
require 'multi_json'
|
6
|
+
|
7
|
+
def get_credentials
|
8
|
+
connection = Faraday.new(:url => "http://localhost:9595") do |c|
|
9
|
+
c.adapter Faraday.default_adapter
|
10
|
+
end
|
11
|
+
credentials_response = connection.post { |req| req.url "/credentials" } # first request is to store the client delta
|
12
|
+
credentials = MultiJson.decode(credentials_response.body)
|
13
|
+
end
|
14
|
+
|
15
|
+
def make_request(id, secret)
|
16
|
+
#make a request using those credentials
|
17
|
+
connection = Faraday.new(:url => "http://localhost:9494") do |c|
|
18
|
+
c.request :hmac_authentication, id, secret
|
19
|
+
c.adapter Faraday.default_adapter
|
20
|
+
end
|
21
|
+
res = connection.get { |req| req.url '/' }
|
22
|
+
[res.status, res.body]
|
23
|
+
end
|
24
|
+
|
25
|
+
def make_valid_request
|
26
|
+
credentials = get_credentials
|
27
|
+
make_request(credentials["id"], credentials["secret"])
|
28
|
+
end
|
29
|
+
|
30
|
+
def make_invalid_request
|
31
|
+
credentials = get_credentials
|
32
|
+
make_request(credentials["id"], "XXX")
|
33
|
+
end
|
34
|
+
|
35
|
+
p make_valid_request
|
36
|
+
p make_invalid_request
|
@@ -0,0 +1,46 @@
|
|
1
|
+
#\ -p 9595
|
2
|
+
|
3
|
+
require 'bundler/setup'
|
4
|
+
require 'sinatra/base'
|
5
|
+
require 'mach'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'openssl'
|
8
|
+
require 'base64'
|
9
|
+
require 'multi_json'
|
10
|
+
|
11
|
+
@@credential_store = {}
|
12
|
+
|
13
|
+
class App < Sinatra::Base
|
14
|
+
post '/credentials' do
|
15
|
+
content_type 'application/json'
|
16
|
+
credentials = CredentialsGenerator.generate_id_secret_pair
|
17
|
+
@@credential_store[credentials[:id]] = credentials[:secret]
|
18
|
+
MultiJson.encode(credentials)
|
19
|
+
end
|
20
|
+
|
21
|
+
get '/credentials/:id' do |id|
|
22
|
+
MultiJson.encode({:id => id, :secret => @@credential_store[id]})
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
class CredentialsGenerator
|
28
|
+
class << self
|
29
|
+
def generate_id
|
30
|
+
SecureRandom.base64(24).tr('+/=lIO0', 'pqrsxyz')
|
31
|
+
end
|
32
|
+
|
33
|
+
def generate_secret
|
34
|
+
Base64.strict_encode64(OpenSSL::Cipher::Cipher.new('aes-256-cbc').random_key)
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_id_secret_pair
|
38
|
+
id = generate_id
|
39
|
+
secret = generate_secret
|
40
|
+
{:id => id, :secret => secret}
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
run App.new
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#\ -p 9494
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'sinatra/base'
|
4
|
+
require 'mach'
|
5
|
+
require 'base64'
|
6
|
+
require 'mach/rack/request_validator'
|
7
|
+
require 'credential_store'
|
8
|
+
|
9
|
+
#MAC_ID = "abc"
|
10
|
+
#MAC_KEY = Base64.strict_encode64("123")
|
11
|
+
|
12
|
+
Mach.configuration do |config|
|
13
|
+
config.with_credential_store CredentialStore::Adapter::CredentialServer.new(:server => 'http://localhost:9595/')
|
14
|
+
config.with_stale_request_window 10
|
15
|
+
config.with_data_store :redis, :host => "localhost", :port => "6379"
|
16
|
+
end
|
17
|
+
|
18
|
+
class App < Sinatra::Base
|
19
|
+
use Mach::Rack::RequestValidator
|
20
|
+
|
21
|
+
get '/' do
|
22
|
+
p (Mach::RequestValidator.valid?(request)) ? "Request is valid" : "Request is not valid"
|
23
|
+
'OK'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
run App.new
|
data/lib/mach.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'mach/faraday/request/hmac_authentication'
|
2
|
+
require 'mach/configuration'
|
3
|
+
require 'mach/validation/request_validator'
|
4
|
+
|
5
|
+
module Mach
|
6
|
+
if ::Faraday.respond_to? :register_middleware
|
7
|
+
::Faraday.register_middleware :request, :hmac_authentication => lambda { Mach::Faraday::HmacAuthentication }
|
8
|
+
end
|
9
|
+
class << self
|
10
|
+
def configuration(&block)
|
11
|
+
@configuration ||= Mach::Configuration.new
|
12
|
+
if block_given?
|
13
|
+
block.call(@configuration)
|
14
|
+
else
|
15
|
+
@configuration
|
16
|
+
end
|
17
|
+
end
|
18
|
+
alias :config :configuration # can use either config or configuration
|
19
|
+
|
20
|
+
def respond_to?(method, include_private=false)
|
21
|
+
self.configuration.respond_to?(method, include_private) || super
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def method_missing(method, *args, &block)
|
27
|
+
return super unless self.configuration.respond_to?(method)
|
28
|
+
self.configuration.send(method, *args, &block)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'mach/timestamp'
|
2
|
+
require 'mach/nonce'
|
3
|
+
require 'mach/normalized_string'
|
4
|
+
require 'mach/signature'
|
5
|
+
|
6
|
+
module Mach
|
7
|
+
class AuthorizationHeader
|
8
|
+
def initialize(id, key, options = {})
|
9
|
+
@id = id
|
10
|
+
@key = key
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
"MAC #{build_auth_value}"
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def build_auth_value
|
21
|
+
timestamp = create_timestamp
|
22
|
+
nonce = create_nonce_for(timestamp)
|
23
|
+
normalized_string = NormalizedString.new(:timestamp => timestamp,
|
24
|
+
:nonce => nonce,
|
25
|
+
:request_method => @options[:request_method],
|
26
|
+
:path => @options[:path],
|
27
|
+
:host => @options[:host],
|
28
|
+
:port => @options[:port]
|
29
|
+
)
|
30
|
+
mac = Mach::Signature.new(@key, normalized_string.to_s) #sign_normalized_string(env, timestamp, nonce)
|
31
|
+
"id=\"#{@id}\",ts=\"#{timestamp}\",nonce=\"#{nonce_for_header(nonce)}\",mac=\"#{mac}\""
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_timestamp
|
35
|
+
Mach::Timestamp.now
|
36
|
+
end
|
37
|
+
|
38
|
+
def create_nonce_for(timestamp)
|
39
|
+
Mach::Nonce.for(timestamp)
|
40
|
+
end
|
41
|
+
|
42
|
+
def nonce_for_header(actual_nonce)
|
43
|
+
actual_nonce
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'mach/persistence/in_memory_store'
|
2
|
+
require 'mach/persistence/redis_store'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Mach
|
6
|
+
class Configuration
|
7
|
+
|
8
|
+
attr_reader :credential_store, :data_store, :stale_request_window
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@stale_request_window = 10
|
12
|
+
@data_store = Mach::Persistence::InMemoryStore.configure({})
|
13
|
+
@credential_store = Hash.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_credential_store(store, options = {})
|
17
|
+
@credential_store = store
|
18
|
+
#store_class = CredentialStore::Adapter.const_get(camelize(store_type.to_s))
|
19
|
+
#@credential_store = store_class.new(options)
|
20
|
+
# find out if we should use redis store first
|
21
|
+
#@credential_store = CredentialStore::Adapter::Redis.new() #todo pass options
|
22
|
+
end
|
23
|
+
|
24
|
+
def with_data_store(store_identifier, options = {})
|
25
|
+
store_class = Mach::Persistence.const_get(camelize("#{store_identifier.to_s}_store"))
|
26
|
+
@data_store = store_class.configure(options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_stale_request_window(num_seconds)
|
30
|
+
@stale_request_window = num_seconds
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def camelize(string)
|
35
|
+
string.split(/[^a-z0-9]/i).map{|w| w.capitalize}.join
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
data/lib/mach/delta.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
module Mach
|
2
|
+
class Delta
|
3
|
+
class << self
|
4
|
+
def present_for(credential_id)
|
5
|
+
Mach.configuration.data_store.find_delta_by(credential_id)
|
6
|
+
end
|
7
|
+
|
8
|
+
def create(credential_id, server_timestamp, client_timestamp)
|
9
|
+
delta_value = server_timestamp - client_timestamp
|
10
|
+
expires_in = Mach.configuration.stale_request_window
|
11
|
+
Mach.configuration.data_store.add_delta(credential_id, delta_value, expires_in)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'mach/hmac'
|
3
|
+
require 'mach/authorization_header'
|
4
|
+
|
5
|
+
module Mach
|
6
|
+
module Faraday
|
7
|
+
class HmacAuthentication < ::Faraday::Middleware
|
8
|
+
include Mach::HMAC
|
9
|
+
KEY = "Authorization".freeze
|
10
|
+
|
11
|
+
def initialize(app, id, key)
|
12
|
+
@mac_id = id
|
13
|
+
@mac_key = key
|
14
|
+
super(app)
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
unless env[:request_headers][KEY]
|
19
|
+
env[:request_headers][KEY] = self.header(env)
|
20
|
+
end
|
21
|
+
@app.call(env)
|
22
|
+
end
|
23
|
+
|
24
|
+
def header(env)
|
25
|
+
AuthorizationHeader.new(@mac_id, @mac_key,
|
26
|
+
:request_method => mac_request_method(request_method(env)),
|
27
|
+
:path => mac_path(path(env), query_string(env)),
|
28
|
+
:host => mac_host(host(env)),
|
29
|
+
:port => mac_port(port(env), scheme(env)),
|
30
|
+
:ext => mac_ext(ext(env))).to_s
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def request_method(env)
|
35
|
+
env[:method]
|
36
|
+
end
|
37
|
+
|
38
|
+
def host(env)
|
39
|
+
env[:url].host
|
40
|
+
end
|
41
|
+
|
42
|
+
def port(env)
|
43
|
+
env[:url].port
|
44
|
+
end
|
45
|
+
|
46
|
+
def path(env)
|
47
|
+
env[:url].path
|
48
|
+
end
|
49
|
+
|
50
|
+
def query_string(env)
|
51
|
+
env[:url].query
|
52
|
+
end
|
53
|
+
|
54
|
+
def ext(env)
|
55
|
+
nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def scheme(env)
|
59
|
+
env[:url].scheme
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/mach/hmac.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Mach
|
2
|
+
module HMAC
|
3
|
+
def mac_request_method(request_method)
|
4
|
+
request_method.to_s.upcase
|
5
|
+
end
|
6
|
+
|
7
|
+
def mac_path(path, query_string)
|
8
|
+
query_string && !query_string.empty? ? "#{path}?#{query_string}" : "#{path}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def mac_host(host)
|
12
|
+
host
|
13
|
+
end
|
14
|
+
|
15
|
+
def mac_port(port, scheme)
|
16
|
+
unless port
|
17
|
+
port = 80 if scheme == 'http'
|
18
|
+
port = 443 if scheme == 'https'
|
19
|
+
end
|
20
|
+
port
|
21
|
+
end
|
22
|
+
|
23
|
+
def mac_ext(ext)
|
24
|
+
ext
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/mach/nonce.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Mach
|
5
|
+
class Nonce
|
6
|
+
class << self
|
7
|
+
def for(timestamp)
|
8
|
+
#17 byte string to make sure we don't get any padding after base64
|
9
|
+
Base64.urlsafe_encode64("#{SecureRandom.random_bytes(17)}#{timestamp}")
|
10
|
+
end
|
11
|
+
|
12
|
+
def exists?(credential_id, nonce_value)
|
13
|
+
Mach.configuration.data_store.find_nonce_by(credential_id, nonce_value)
|
14
|
+
end
|
15
|
+
|
16
|
+
def persist(credential_id, nonce_value, timestamp)
|
17
|
+
expires_in = Mach.configuration.stale_request_window
|
18
|
+
Mach.configuration.data_store.add_nonce(credential_id, nonce_value, expires_in)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Mach
|
2
|
+
class NormalizedString
|
3
|
+
def initialize(options = {})
|
4
|
+
@timestamp = options[:timestamp]
|
5
|
+
@nonce = options[:nonce]
|
6
|
+
@request_method = options[:request_method]
|
7
|
+
@path = options[:path]
|
8
|
+
@host = options[:host]
|
9
|
+
@port = options[:port]
|
10
|
+
@ext = options[:ext] || "\n"
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
[@timestamp, @nonce, @request_method, @path, @host, @port, @ext].join("\n")
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Mach
|
2
|
+
module Persistence
|
3
|
+
class DeltaAndNonceStore
|
4
|
+
class << self
|
5
|
+
def configure(options = {})
|
6
|
+
self.new(options)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def find_delta_by(credential_id)
|
11
|
+
raise "Implement me"
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_delta(credential_id, delta_value, expires_in)
|
15
|
+
raise "Implement me"
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_nonce_by(credential_id, nonce_value)
|
19
|
+
raise "Implement me"
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_nonce(credential_id, nonce_value, timestamp)
|
23
|
+
raise "Implement me"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'mach/persistence/delta_and_nonce_store'
|
2
|
+
require 'mach/timestamp'
|
3
|
+
|
4
|
+
module Mach
|
5
|
+
module Persistence
|
6
|
+
class InMemoryStore < Mach::Persistence::DeltaAndNonceStore
|
7
|
+
def initialize(options = {})
|
8
|
+
@deltas = {}
|
9
|
+
@nonces = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_delta_by(credential_id)
|
13
|
+
delta_hash = @deltas[credential_id] || {}
|
14
|
+
now = Timestamp.now
|
15
|
+
if delta_hash[:expires_at] && delta_hash[:expires_at] > now
|
16
|
+
delta_hash[:delta_value]
|
17
|
+
else
|
18
|
+
@deltas[credential_id] = nil
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_delta(credential_id, delta_value, expires_in)
|
24
|
+
expires_at = Timestamp.now + expires_in
|
25
|
+
@deltas[credential_id] = {:delta_value => delta_value, :expires_at => expires_at}
|
26
|
+
@deltas[credential_id][:delta_value]
|
27
|
+
end
|
28
|
+
|
29
|
+
def find_nonce_by(credential_id, nonce_value)
|
30
|
+
nonces_for_credential_id = @nonces[credential_id] || {}
|
31
|
+
expires_at = nonces_for_credential_id[nonce_value]
|
32
|
+
now = Timestamp.now
|
33
|
+
if expires_at && expires_at > now
|
34
|
+
nonce_value
|
35
|
+
else
|
36
|
+
nonces_for_credential_id[nonce_value] = nil
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def add_nonce(credential_id, nonce_value, expires_in)
|
42
|
+
expires_at = Timestamp.now + expires_in
|
43
|
+
@nonces[credential_id] ||= {}
|
44
|
+
@nonces[credential_id][nonce_value] = expires_at
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'mach/persistence/delta_and_nonce_store'
|
2
|
+
require 'mach/timestamp'
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
module Mach
|
6
|
+
module Persistence
|
7
|
+
class RedisStore < Mach::Persistence::DeltaAndNonceStore
|
8
|
+
def initialize(options = {})
|
9
|
+
raise MissingConfigurationOptionError unless options[:host] && options[:port]
|
10
|
+
@redis = Redis.new(:host => options[:host], :port => options[:port])
|
11
|
+
end
|
12
|
+
|
13
|
+
def find_delta_by(credential_id)
|
14
|
+
@redis.get(delta_key_for(credential_id))
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_delta(credential_id, delta_value, expires_in)
|
18
|
+
@redis.setex(delta_key_for(credential_id), expires_in, delta_value)
|
19
|
+
delta_value
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_nonce_by(credential_id, nonce_value)
|
23
|
+
@redis.get(nonce_key_for(credential_id, nonce_value))
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_nonce(credential_id, nonce_value, expires_in)
|
27
|
+
@redis.setex(nonce_key_for(credential_id, nonce_value), expires_in, 1)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
def delta_key_for(credential_id)
|
32
|
+
"delta_key_for_#{credential_id}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def nonce_key_for(credential_id, nonce)
|
36
|
+
"nonce_key_for_#{credential_id}_#{nonce}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Mach
|
2
|
+
module Rack
|
3
|
+
class RequestValidator
|
4
|
+
def initialize app
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call env
|
9
|
+
request = Mach::Request.new(env)
|
10
|
+
if request.mac_authorization? && !Mach::RequestValidator.valid?(request)
|
11
|
+
failure
|
12
|
+
else
|
13
|
+
success(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def success(env)
|
20
|
+
@app.call(env)
|
21
|
+
end
|
22
|
+
|
23
|
+
def failure
|
24
|
+
[401, { 'Content-Type' => 'application/json' }, ['Unauthorized'] ]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
data/lib/mach/request.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'mach/normalized_string'
|
3
|
+
|
4
|
+
module Mach
|
5
|
+
class Request < ::Rack::Request
|
6
|
+
include Mach::HMAC
|
7
|
+
|
8
|
+
def authorization
|
9
|
+
@env['HTTP_AUTHORIZATION']
|
10
|
+
end
|
11
|
+
|
12
|
+
def mac_authorization?
|
13
|
+
!!(self.authorization =~ /^\s*MAC .*$/)
|
14
|
+
end
|
15
|
+
|
16
|
+
def mac_id
|
17
|
+
self.authorization.scan(/^\s*MAC\s*.*id="(.*?)".*/).flatten[0]
|
18
|
+
end
|
19
|
+
|
20
|
+
def mac_timestamp
|
21
|
+
self.authorization.scan(/^\s*MAC\s*.*ts="(.*?)".*/).flatten[0]
|
22
|
+
end
|
23
|
+
|
24
|
+
def mac_nonce
|
25
|
+
self.authorization.scan(/^\s*MAC\s*.*nonce="(.*?)".*/).flatten[0]
|
26
|
+
end
|
27
|
+
|
28
|
+
def mac_ext
|
29
|
+
self.authorization.scan(/^\s*MAC\s*.*ext="(.*?)".*/).flatten[0]
|
30
|
+
end
|
31
|
+
|
32
|
+
def mac_signature
|
33
|
+
self.authorization.scan(/^\s*MAC\s*.*mac="(.*?)".*/).flatten[0]
|
34
|
+
end
|
35
|
+
|
36
|
+
def mac_normalized_request_string
|
37
|
+
if mac_authorization?
|
38
|
+
NormalizedString.new(:timestamp => mac_timestamp,
|
39
|
+
:nonce => mac_nonce,
|
40
|
+
:request_method => request_method,
|
41
|
+
:path => mac_path(path, query_string),
|
42
|
+
:host => host,
|
43
|
+
:port => mac_port(self.port, self.scheme),
|
44
|
+
:ext => mac_ext
|
45
|
+
).to_s
|
46
|
+
else
|
47
|
+
""
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Mach
|
5
|
+
class Signature
|
6
|
+
ALGORITHMS = {'hmac-sha-256' => 'sha256', 'hmac-sha-1' => 'sha1'}
|
7
|
+
DEFAULT_ALGORITHM = 'hmac-sha-256'
|
8
|
+
|
9
|
+
def initialize(key, data, algorithm = DEFAULT_ALGORITHM)
|
10
|
+
@algorithm = algorithm
|
11
|
+
@data = data
|
12
|
+
@key = key
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
Base64.strict_encode64(OpenSSL::HMAC.digest(digest, @key, @data))
|
17
|
+
end
|
18
|
+
|
19
|
+
def matches?(base64_expected_signature)
|
20
|
+
to_s == base64_expected_signature
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def digest
|
26
|
+
digest_algorithm = @algorithm ? ALGORITHMS[@algorithm] : ALGORITHMS[DEFAULT_ALGORITHM]
|
27
|
+
OpenSSL::Digest::Digest.new(digest_algorithm)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'mach/error/error'
|
2
|
+
require 'mach/request'
|
3
|
+
require 'mach/validation/timestamp_validator'
|
4
|
+
require 'mach/validation/nonce_validator'
|
5
|
+
require 'mach/validation/signature_validator'
|
6
|
+
|
7
|
+
module Mach
|
8
|
+
class RequestValidator
|
9
|
+
class << self
|
10
|
+
def valid?(rack_request)
|
11
|
+
hmac_request = Mach::Request.new(rack_request.env)
|
12
|
+
raise Mach::Error::RequestNotMacAuthenticatedError unless hmac_request.mac_authorization?
|
13
|
+
valid = hmac_request.mac_id &&
|
14
|
+
Mach::Validation::TimestampValidator.valid?(hmac_request) &&
|
15
|
+
Mach::Validation::NonceValidator.valid?(hmac_request) &&
|
16
|
+
Mach::Validation::SignatureValidator.valid?(hmac_request)
|
17
|
+
#need to make sure we store the nonce
|
18
|
+
Nonce.persist(hmac_request.mac_id, hmac_request.mac_nonce, hmac_request.mac_timestamp.to_i) if valid
|
19
|
+
valid
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'mach/signature'
|
2
|
+
|
3
|
+
module Mach
|
4
|
+
module Validation
|
5
|
+
|
6
|
+
class SignatureValidator
|
7
|
+
class << self
|
8
|
+
def valid?(hmac_request)
|
9
|
+
if secret = credential_store[hmac_request.mac_id]
|
10
|
+
data = hmac_request.mac_normalized_request_string
|
11
|
+
Mach::Signature.new(secret, data).matches?(hmac_request.mac_signature)
|
12
|
+
else
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def credential_store
|
19
|
+
Mach::configuration.credential_store
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'mach/timestamp'
|
2
|
+
require 'mach/delta'
|
3
|
+
|
4
|
+
module Mach
|
5
|
+
module Validation
|
6
|
+
class TimestampValidator
|
7
|
+
class << self
|
8
|
+
def valid?(hmac_request)
|
9
|
+
server_timestamp = Mach::Timestamp.now.to_i
|
10
|
+
client_timestamp = hmac_request.mac_timestamp.to_i
|
11
|
+
delta = Mach::Delta.present_for(hmac_request.mac_id) #do we have a delta for this client
|
12
|
+
unless delta
|
13
|
+
delta = Mach::Delta.create(hmac_request.mac_id, server_timestamp, client_timestamp)
|
14
|
+
end
|
15
|
+
#make sure the client timestamp is not older than what we're willing to accept
|
16
|
+
server_timestamp - (client_timestamp + delta.to_i) < Mach.configuration.stale_request_window
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/mach/version.rb
ADDED
data/mach.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "mach/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "mach"
|
7
|
+
s.version = Mach::VERSION
|
8
|
+
s.authors = ["Alan Skorkin"]
|
9
|
+
s.email = ["alan@skorks.com"]
|
10
|
+
s.homepage = "https://github.com/skorks/mach"
|
11
|
+
s.summary = %q{HMAC authentication stuff}
|
12
|
+
s.description = %q{HMAC authentication stuff}
|
13
|
+
|
14
|
+
s.rubyforge_project = "mach"
|
15
|
+
|
16
|
+
s.add_dependency 'faraday'
|
17
|
+
s.add_dependency 'rack'
|
18
|
+
s.add_dependency 'multi_json'
|
19
|
+
s.add_dependency 'redis'
|
20
|
+
|
21
|
+
s.add_development_dependency 'rake'
|
22
|
+
s.add_development_dependency 'rspec'
|
23
|
+
s.add_development_dependency 'json'
|
24
|
+
|
25
|
+
s.files = `git ls-files`.split("\n")
|
26
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
27
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
28
|
+
s.require_paths = ["lib"]
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require 'mach/normalized_string'
|
3
|
+
|
4
|
+
describe Mach::NormalizedString do
|
5
|
+
let(:timestamp) {1344824119}
|
6
|
+
let(:nonce) {"abc123"}
|
7
|
+
let(:request_method) {"GET"}
|
8
|
+
let(:path) {"/blah?yadda=5&foo=bar"}
|
9
|
+
let(:host) {"blah.com"}
|
10
|
+
let(:port) {80}
|
11
|
+
let(:ext) {"hello"}
|
12
|
+
|
13
|
+
let(:normalized_string) do
|
14
|
+
Mach::NormalizedString.new(:timestamp => timestamp,
|
15
|
+
:nonce => nonce,
|
16
|
+
:request_method => request_method,
|
17
|
+
:path => path,
|
18
|
+
:host => host,
|
19
|
+
:port => port,
|
20
|
+
:ext => ext
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#to_s" do
|
25
|
+
subject { normalized_string.to_s }
|
26
|
+
context "when all compenents present" do
|
27
|
+
it { subject.should == "#{timestamp}\n#{nonce}\n#{request_method}\n#{path}\n#{host}\n#{port}\n#{ext}" }
|
28
|
+
end
|
29
|
+
context "when no ext" do
|
30
|
+
let(:ext) {nil}
|
31
|
+
it { subject.should == "#{timestamp}\n#{nonce}\n#{request_method}\n#{path}\n#{host}\n#{port}\n\n" }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require File.expand_path('../spec_helper', __FILE__)
|
2
|
+
require "base64"
|
3
|
+
require 'mach/signature'
|
4
|
+
|
5
|
+
describe Mach::Signature do
|
6
|
+
let(:key) { "some_key" }
|
7
|
+
let(:data) {"hello world"}
|
8
|
+
|
9
|
+
describe "#to_s" do
|
10
|
+
subject { Mach::Signature.new(key, data).to_s }
|
11
|
+
|
12
|
+
context "produces a signature" do
|
13
|
+
it { ->{subject}.should_not raise_error }
|
14
|
+
end
|
15
|
+
#this was to test ios integration and it seemed to work
|
16
|
+
#context "testing123" do
|
17
|
+
#let(:timestamp) {1344843814}
|
18
|
+
#let(:nonce) {"PYVV2cPkVvVsB0Gi"}
|
19
|
+
#let(:request_method) {"GET"}
|
20
|
+
#let(:path) {"/contests/contest-20120191956?subject_path=%2Fcontests%2F20120191956"}
|
21
|
+
#let(:host) {"staging.api.playupdev.com"}
|
22
|
+
#let(:port) {80}
|
23
|
+
|
24
|
+
#let(:data) do
|
25
|
+
#Mach::NormalizedString.new(:timestamp => timestamp,
|
26
|
+
#:nonce => nonce,
|
27
|
+
#:request_method => request_method,
|
28
|
+
#:path => path,
|
29
|
+
#:host => host,
|
30
|
+
#:port => port
|
31
|
+
#).to_s
|
32
|
+
#end
|
33
|
+
|
34
|
+
#let(:key) { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" }
|
35
|
+
#it {subject.should == "YnHx40PpfjMaxtO+sxJvg2XtOF70L1zGlNad92dg8i4="}
|
36
|
+
#end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe "#matches?" do
|
40
|
+
let(:signature) { Mach::Signature.new(key, data) }
|
41
|
+
subject { signature.matches?(expected_signature) }
|
42
|
+
|
43
|
+
|
44
|
+
context "when expected signature is invalid" do
|
45
|
+
let(:expected_signature) { "abc123" }
|
46
|
+
it {subject.should_not be_true}
|
47
|
+
end
|
48
|
+
context "when expected signature is valid" do
|
49
|
+
let(:expected_signature) { '/wfbnU08rHnneh2Q4wSopDyULH43ePyuSyHMBc9nbnw=' }
|
50
|
+
|
51
|
+
it {subject.should be_true}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
$:.unshift(File.expand_path('../', __FILE__))
|
3
|
+
$:.unshift(File.expand_path('../../lib', __FILE__))
|
4
|
+
|
5
|
+
require 'mach'
|
6
|
+
|
7
|
+
# Requires supporting ruby files with custom matchers and macros, etc,
|
8
|
+
# in spec/support/ and its subdirectories.
|
9
|
+
Dir[File.join(File.expand_path('../support', __FILE__), '**/*.rb')].each {|f| require f}
|
10
|
+
|
11
|
+
# Use Mocha to mock with RSpec
|
12
|
+
RSpec.configure do |config|
|
13
|
+
config.mock_with :rspec
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,200 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mach
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Alan Skorkin
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-30 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: faraday
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rack
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: multi_json
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: redis
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rake
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rspec
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: json
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: HMAC authentication stuff
|
127
|
+
email:
|
128
|
+
- alan@skorks.com
|
129
|
+
executables: []
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- .gitignore
|
134
|
+
- .rspec
|
135
|
+
- .rvmrc
|
136
|
+
- Gemfile
|
137
|
+
- README.md
|
138
|
+
- Rakefile
|
139
|
+
- examples/Gemfile
|
140
|
+
- examples/Gemfile.lock
|
141
|
+
- examples/Rakefile
|
142
|
+
- examples/client.rb
|
143
|
+
- examples/credential_server.ru
|
144
|
+
- examples/validating_server.ru
|
145
|
+
- lib/mach.rb
|
146
|
+
- lib/mach/authorization_header.rb
|
147
|
+
- lib/mach/configuration.rb
|
148
|
+
- lib/mach/delta.rb
|
149
|
+
- lib/mach/error/error.rb
|
150
|
+
- lib/mach/faraday/request/hmac_authentication.rb
|
151
|
+
- lib/mach/hmac.rb
|
152
|
+
- lib/mach/nonce.rb
|
153
|
+
- lib/mach/normalized_string.rb
|
154
|
+
- lib/mach/persistence/delta_and_nonce_store.rb
|
155
|
+
- lib/mach/persistence/in_memory_store.rb
|
156
|
+
- lib/mach/persistence/redis_store.rb
|
157
|
+
- lib/mach/rack/request_validator.rb
|
158
|
+
- lib/mach/request.rb
|
159
|
+
- lib/mach/signature.rb
|
160
|
+
- lib/mach/timestamp.rb
|
161
|
+
- lib/mach/validation/nonce_validator.rb
|
162
|
+
- lib/mach/validation/request_validator.rb
|
163
|
+
- lib/mach/validation/signature_validator.rb
|
164
|
+
- lib/mach/validation/timestamp_validator.rb
|
165
|
+
- lib/mach/version.rb
|
166
|
+
- mach.gemspec
|
167
|
+
- spec/normalized_string_spec.rb
|
168
|
+
- spec/signature_spec.rb
|
169
|
+
- spec/spec_helper.rb
|
170
|
+
homepage: https://github.com/skorks/mach
|
171
|
+
licenses: []
|
172
|
+
post_install_message:
|
173
|
+
rdoc_options: []
|
174
|
+
require_paths:
|
175
|
+
- lib
|
176
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
177
|
+
none: false
|
178
|
+
requirements:
|
179
|
+
- - ! '>='
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
182
|
+
segments:
|
183
|
+
- 0
|
184
|
+
hash: 1737604513296395942
|
185
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
186
|
+
none: false
|
187
|
+
requirements:
|
188
|
+
- - ! '>='
|
189
|
+
- !ruby/object:Gem::Version
|
190
|
+
version: '0'
|
191
|
+
segments:
|
192
|
+
- 0
|
193
|
+
hash: 1737604513296395942
|
194
|
+
requirements: []
|
195
|
+
rubyforge_project: mach
|
196
|
+
rubygems_version: 1.8.24
|
197
|
+
signing_key:
|
198
|
+
specification_version: 3
|
199
|
+
summary: HMAC authentication stuff
|
200
|
+
test_files: []
|