mach 0.0.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/.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: []
|