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 +18 -0
- data/.travis.yml +9 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +34 -0
- data/Rakefile +10 -0
- data/lib/rack/signature.rb +12 -0
- data/lib/rack/signature/build_message.rb +41 -0
- data/lib/rack/signature/hmac_signature.rb +41 -0
- data/lib/rack/signature/verify.rb +56 -0
- data/lib/rack/signature/version.rb +11 -0
- data/rack-signature.gemspec +25 -0
- data/test/build_message_test.rb +28 -0
- data/test/hmac_signature_test.rb +44 -0
- data/test/test_helper.rb +4 -0
- data/test/verify_test.rb +83 -0
- data/test/version_test.rb +9 -0
- metadata +132 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
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
|
+
[](https://travis-ci.org/revans/rack-signature)
|
2
|
+
[](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,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,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
|
data/test/test_helper.rb
ADDED
data/test/verify_test.rb
ADDED
@@ -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
|
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:
|