rack-signature 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 ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .DS_Store
data/.travis.yml ADDED
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
5
+ before_install:
6
+ - gem install bundler
7
+ notifications:
8
+ recipients:
9
+ - robert@codewranglers.org
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Robert Evans
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,34 @@
1
+ [![Build Status](https://secure.travis-ci.org/revans/rack-signature.png)](https://travis-ci.org/revans/rack-signature)
2
+ [![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/revans/rack-signature)
3
+
4
+ # Rack::Signature
5
+
6
+ Rack Middleware used to verify signed requests.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ gem 'rack-signature'
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install rack-signature
21
+
22
+ ## Usage
23
+
24
+ ```ruby
25
+ use Rack::Signature, 'your-shared-key'
26
+ ```
27
+
28
+ ## Contributing
29
+
30
+ 1. Fork it
31
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
32
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
33
+ 4. Push to the branch (`git push origin my-new-feature`)
34
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.test_files = FileList['test/*_test.rb']
7
+ t.verbose = true
8
+ end
9
+
10
+ task :default => 'test'
@@ -0,0 +1,12 @@
1
+ require_relative 'signature/version'
2
+ require_relative 'signature/build_message'
3
+ require_relative 'signature/hmac_signature'
4
+ require_relative 'signature/verify'
5
+
6
+ module Rack
7
+ module Signature
8
+ def self.new(app, key)
9
+ Verify.new(app, key)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,41 @@
1
+ require 'rack/request'
2
+
3
+ module Rack
4
+ module Signature
5
+ class BuildMessage
6
+ attr_reader :request
7
+
8
+ # initialize with a hash of options
9
+ #
10
+ # ==== Attributes
11
+ #
12
+ # * +env+ - The rack app env
13
+ #
14
+ def initialize(env)
15
+ @request = ::Rack::Request.new(env)
16
+ end
17
+
18
+ def build!
19
+ create_request_message
20
+ end
21
+
22
+ private
23
+
24
+ def sort_query_params
25
+ request.params.sort.map { |param| param.join('=') }
26
+ end
27
+
28
+ def canonicalized_query_params
29
+ sort_query_params.join('&')
30
+ end
31
+
32
+ def create_request_message
33
+ request.request_method.upcase +
34
+ request.path_info.downcase +
35
+ request.host.downcase +
36
+ canonicalized_query_params
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,41 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Rack
5
+ module Signature
6
+ class HmacSignature
7
+
8
+ # initialize with the shared key and a hash of options for building the
9
+ # signature
10
+ #
11
+ # ==== Attributes
12
+ #
13
+ # * +key+ - The shared key used as a salt.
14
+ # * +message+ - The built request message
15
+ #
16
+ def initialize(key, message)
17
+ @key, @message = key, message
18
+ end
19
+
20
+ # returns a Base64 encoded HMAC of the request plus the private shared key
21
+ def sign
22
+ encode_hmac
23
+ end
24
+
25
+ private
26
+
27
+ def encode_hmac
28
+ Base64.encode64( hmac_message ).chomp
29
+ end
30
+
31
+ def hmac_message
32
+ ::OpenSSL::HMAC.digest(cipher, @key, @message)
33
+ end
34
+
35
+ def cipher
36
+ ::OpenSSL::Digest::Digest.new("sha256")
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,56 @@
1
+ require_relative 'hmac_signature'
2
+
3
+ # A Rack app to verify requests based on a computed signature passed within the
4
+ # HTTP Header: X-Auth-Sig.
5
+ #
6
+ # This app will rebuild the signature and then compare its own computed HMAC
7
+ # against the one sent from the client to verify authenticity.
8
+ #
9
+ module Rack
10
+ module Signature
11
+ class Verify
12
+
13
+ # Initializes the Rack Middleware
14
+ #
15
+ # ==== Attributes
16
+ #
17
+ # * +app+ - A Rack app
18
+ # * +key+ - The shared key used as a salt.
19
+ #
20
+ def initialize(app, key)
21
+ @app, @key = app, key
22
+ end
23
+
24
+ def call(env)
25
+ if signature_is_valid?(env)
26
+ @app.call(env)
27
+ else
28
+ invalid_signature
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ # if the signature is invalid we send back this Rack app
35
+ def invalid_signature
36
+ [403, {'Content-Type' => 'text/html'}, 'Invalid Signature']
37
+ end
38
+
39
+ # compares the received Signature against what the Signature should be
40
+ # (computed signature)
41
+ def signature_is_valid?(env)
42
+ received_signature = env["HTTP_X_AUTH_SIG"]
43
+ expected_signature = compute_signature(env)
44
+
45
+ expected_signature == received_signature
46
+ end
47
+
48
+ # builds the request message and tells HmacSignature to sign the message
49
+ def compute_signature(env)
50
+ message = BuildMessage.new(env).build!
51
+ HmacSignature.new(@key, message).sign
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,11 @@
1
+ module Rack
2
+ module Signature
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ PATCH = 1
6
+
7
+ def self.version
8
+ [MAJOR, MINOR, PATCH].join('.')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'rack/signature/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "rack-signature"
8
+ gem.version = Rack::Signature.version
9
+ gem.authors = ["Robert Evans"]
10
+ gem.email = ["robert@codewranglers.org"]
11
+ gem.description = %q{Rack Middleware for verifying signed requests}
12
+ gem.summary = %q{Rack Middleware for verifying signed requests}
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'rack'
21
+
22
+ gem.add_development_dependency 'minitest'
23
+ gem.add_development_dependency 'rack-test'
24
+ gem.add_development_dependency 'rake'
25
+ end
@@ -0,0 +1,28 @@
1
+ require_relative '../lib/rack/signature/build_message'
2
+ require 'test_helper'
3
+
4
+ module Rack::Signature
5
+ class BuildMessageTest < MiniTest::Unit::TestCase
6
+
7
+ def test_build_with_a_valid_request
8
+ env = Rack::MockRequest.env_for(
9
+ "http://example.com/api/login?password=123456&email=me@home.com")
10
+
11
+ assert_equal "GET/api/loginexample.comemail=me@home.com&password=123456",
12
+ BuildMessage.new(env).build!
13
+ end
14
+
15
+ def test_build_order
16
+ env = Rack::MockRequest.env_for(
17
+ "http://example.com/api/login",
18
+ "Content-Type" => "application/json",
19
+ "REQUEST_METHOD" => "POST",
20
+ input: "password=123456&email=me@home.com&name=me&age=1"
21
+ )
22
+
23
+ assert_equal "POST/api/loginexample.comage=1&email=me@home.com&name=me&password=123456",
24
+ BuildMessage.new(env).build!
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ require_relative '../lib/rack/signature/hmac_signature'
2
+ require 'test_helper'
3
+
4
+ module Rack::Signature
5
+ class HmacSignatureTest < MiniTest::Unit::TestCase
6
+
7
+ def test_valid_signature
8
+ assert_equal expected_signature,
9
+ HmacSignature.new(key, request_message).sign
10
+ end
11
+
12
+ def test_tampered_query_params
13
+ tampered_message = "POST/api/loginexample.comage=1&email=me@home.com&name=me&password=3456"
14
+
15
+ refute_equal expected_signature,
16
+ HmacSignature.new(key, tampered_message).sign
17
+ end
18
+
19
+ def test_different_shared_key
20
+ refute_equal expected_signature,
21
+ HmacSignature.new("123", request_message).sign
22
+ end
23
+
24
+ def test_missing_options
25
+ missing_request_params = "POST/api/loginexample.comemail=me@home.com&password=123456"
26
+ refute_equal expected_signature,
27
+ HmacSignature.new(key, missing_request_params).sign
28
+ end
29
+
30
+ # Helper methods
31
+ def request_message
32
+ "POST/api/loginexample.comage=1&email=me@home.com&name=me&password=123456"
33
+ end
34
+
35
+ def key
36
+ ::Digest::SHA2.hexdigest("shared-key")
37
+ end
38
+
39
+ def expected_signature
40
+ "Z0qY8Hy4a/gJkGZI0gklzM6vZztsAVVDjA18vb1BvHg="
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,4 @@
1
+ require 'minitest/autorun'
2
+ require 'rack/test'
3
+ require 'rack/mock'
4
+ require 'digest/sha2'
@@ -0,0 +1,83 @@
1
+ require_relative '../lib/rack/signature'
2
+ require 'test_helper'
3
+
4
+ describe "Verifying a signed request" do
5
+ include Rack::Test::Methods
6
+
7
+ def setup
8
+ @shared_key = key
9
+ @signature = expected_signature
10
+ end
11
+
12
+ let(:app) { lambda { |env| [200, {}, ['Hello World']] } }
13
+ let(:rack_signature) { Rack::Signature.new(app, @shared_key) }
14
+ let(:mock_request) { Rack::MockRequest.new(rack_signature) }
15
+
16
+ describe "when a request is made without a signature" do
17
+ let(:response) { mock_request.get '/api/login?password=123456&email=me@home.com' }
18
+
19
+ it 'returns a 403 status' do
20
+ assert_equal 403, response.status
21
+ end
22
+
23
+ it 'returns "Invalid Signature" as the response body' do
24
+ assert_equal 'Invalid Signature', response.body
25
+ end
26
+
27
+ it 'returns the correct header' do
28
+ expected_header = {"Content-Type"=>"text/html", "Content-Length"=>"17"}
29
+ assert_equal expected_header, response.header
30
+ end
31
+ end
32
+
33
+ describe "when a requests is sent with a valid signature" do
34
+ let(:response) do
35
+ mock_request.post("http://example.com/api/login",
36
+ "Content-Type" => "application/json",
37
+ "REQUEST_METHOD" => "POST",
38
+ "HTTP_X_AUTH_SIG" => @signature,
39
+ input: "password=123456&email=me@home.com&name=me&age=1")
40
+ end
41
+
42
+ it 'will return a 200 status' do
43
+ assert_equal 200, response.status
44
+ end
45
+
46
+ it 'will call the next rack app' do
47
+ assert_equal 'Hello World', response.body
48
+ end
49
+ end
50
+
51
+ describe "when a requests is sent with a tampered signature" do
52
+ let(:response) do
53
+ mock_request.post("http://example.com/api/login",
54
+ "Content-Type" => "application/json",
55
+ "REQUEST_METHOD" => "POST",
56
+ "HTTP_X_AUTH_SIG" => @signature,
57
+ input: "password=1234567&email=me@home.com&name=me&age=1")
58
+ end
59
+
60
+ it 'returns a 403 status' do
61
+ assert_equal 403, response.status
62
+ end
63
+
64
+ it 'returns "Invalid Signature" as the response body' do
65
+ assert_equal 'Invalid Signature', response.body
66
+ end
67
+
68
+ it 'returns the correct header' do
69
+ expected_header = {"Content-Type"=>"text/html", "Content-Length"=>"17"}
70
+ assert_equal expected_header, response.header
71
+ end
72
+ end
73
+
74
+ # Helper Methods
75
+ def key
76
+ ::Digest::SHA2.hexdigest("shared-key")
77
+ end
78
+
79
+ def expected_signature
80
+ "Z0qY8Hy4a/gJkGZI0gklzM6vZztsAVVDjA18vb1BvHg="
81
+ end
82
+
83
+ end
@@ -0,0 +1,9 @@
1
+ require_relative '../lib/rack/signature/version'
2
+ require 'minitest/autorun'
3
+
4
+ class VersionTest < MiniTest::Unit::TestCase
5
+ def test_version
6
+ expected_version = '0.0.1'
7
+ assert_equal expected_version, Rack::Signature.version
8
+ end
9
+ end
metadata ADDED
@@ -0,0 +1,132 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-signature
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Robert Evans
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-12-12 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rack
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: minitest
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
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: rack-test
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
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: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
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
+ description: Rack Middleware for verifying signed requests
79
+ email:
80
+ - robert@codewranglers.org
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - .gitignore
86
+ - .travis.yml
87
+ - Gemfile
88
+ - LICENSE.txt
89
+ - README.md
90
+ - Rakefile
91
+ - lib/rack/signature.rb
92
+ - lib/rack/signature/build_message.rb
93
+ - lib/rack/signature/hmac_signature.rb
94
+ - lib/rack/signature/verify.rb
95
+ - lib/rack/signature/version.rb
96
+ - rack-signature.gemspec
97
+ - test/build_message_test.rb
98
+ - test/hmac_signature_test.rb
99
+ - test/test_helper.rb
100
+ - test/verify_test.rb
101
+ - test/version_test.rb
102
+ homepage: ''
103
+ licenses: []
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ! '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ required_rubygems_version: !ruby/object:Gem::Requirement
115
+ none: false
116
+ requirements:
117
+ - - ! '>='
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ requirements: []
121
+ rubyforge_project:
122
+ rubygems_version: 1.8.23
123
+ signing_key:
124
+ specification_version: 3
125
+ summary: Rack Middleware for verifying signed requests
126
+ test_files:
127
+ - test/build_message_test.rb
128
+ - test/hmac_signature_test.rb
129
+ - test/test_helper.rb
130
+ - test/verify_test.rb
131
+ - test/version_test.rb
132
+ has_rdoc: